Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ All generated files have `// Code generated by X, DO NOT EDIT.` headers — neve
2. Run `make gen-api` to regenerate OpenAPI spec and SDKs
3. Run `make generate` to regenerate Go server/client code

The TypeSpec JS client emitted from `api/spec/packages/aip` now lands in `api/spec/packages/aip-client-javascript/`. The emitter regenerates `src/`, `README.md`, and five conformance test files (`tests/client.spec.ts`, `tests/meters.spec.ts`, `tests/errors.spec.ts`, `tests/nesting.spec.ts`, `tests/internal.spec.ts`); every regenerated file carries a `Code generated by @openmeter/typespec-typescript. DO NOT EDIT.` header — treat files without that header as hand-written. `package.json` is **stable, hand-maintained, and committed** (the emitter's `writeOutput` only writes the paths it lists, so the manifest survives regeneration). The test suite is vitest + `@fetch-mock/vitest`; keep hand-written tests and helpers in `tests/` (never `src/`), and put test-runner dependencies/scripts in the `api/spec/package.json` workspace root rather than the client package manifest. Static publish metadata (`name`, `license`, `homepage`, `repository`) lives directly in the client `package.json`; only the per-release `version` is injected at publish time. Operations marked `x-private` in the TypeSpec source are omitted from the generated SDK (they stay in the OpenAPI output); operations marked `x-internal` (without `x-private`) are emitted but quarantined under `client.internal.<group>.<method>` (`src/sdk/internal.ts`, an `Internal` aggregate with `Internal<Group>` facade classes; internal ops share their group's `funcs/` and `models/operations/` modules, a group whose ops are all internal gets no public facade, and the `Internal` class is deliberately not re-exported from the package root — the name belongs to the Internal Server Error model). Removing an operation from the spec (or marking it private) leaves stale generated files behind — the emitter never deletes outputs, so `git rm` them.
The TypeSpec JS client emitted from `api/spec/packages/aip` now lands in `api/spec/packages/aip-client-javascript/`. The emitter regenerates `src/`, `README.md`, and five conformance test files (`tests/client.spec.ts`, `tests/meters.spec.ts`, `tests/errors.spec.ts`, `tests/nesting.spec.ts`, `tests/internal.spec.ts`); every regenerated file carries a `Code generated by @openmeter/typespec-typescript. DO NOT EDIT.` header — treat files without that header as hand-written. `package.json` is **stable, hand-maintained, and committed** (the emitter's `writeOutput` only writes the paths it lists, so the manifest survives regeneration). The test suite is vitest + `@fetch-mock/vitest`; keep hand-written tests and helpers in `tests/` (never `src/`), and put test-runner dependencies/scripts in the `api/spec/package.json` workspace root rather than the client package manifest. Static publish metadata (`name`, `license`, `homepage`, `repository`) lives directly in the client `package.json`; only the per-release `version` is injected at publish time. Operations marked `x-internal` or `x-private` in the TypeSpec source are emitted but quarantined under `client.internal.<group>.<method>` (`src/sdk/internal.ts`, an `Internal` aggregate with `Internal<Group>` facade classes; internal ops share their group's `funcs/` and `models/operations/` modules, a group whose ops are all internal gets no public facade, and the `Internal` class is deliberately not re-exported from the package root — the name belongs to the Internal Server Error model). Removing an operation from the spec leaves stale generated files behind — the emitter never deletes outputs, so `git rm` them (and `rm` the gitignored `*.assert.ts` companions, which `git rm` cannot clean up on collaborators' checkouts).

The emitted `api/spec/packages/aip-client-javascript/src/sdk/sdk.ts` exposes aggregated sub-client getters on the `OpenMeter` class (e.g. `events`, `meters`, `customers`, `entitlements`, `subscriptions`, `billing`, `features`, `plans`, `addons`, `planAddons`, `tax`, `defaults`). Access operations through those getters, e.g. `sdk.meters.list()`, `sdk.customers.create(...)`, `sdk.plans.create(...)`. These grouped-client methods throw `HTTPError` on failure; the same operations are also available as tree-shakeable standalone functions under `src/funcs/` that return a `Result` instead of throwing.

Expand Down
29 changes: 13 additions & 16 deletions api/spec/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ Structural rules the interface emitter follows:
- **`extends`** the base interface when the model has a `baseModel`, so inherited
fields/docs propagate (`BadRequest extends BaseError`).
- **Open records** (`...Record<…>`) get an index signature (`[key: string]: V`).
- **Named union aliases.** Every named TypeSpec `union` that is reachable from a
non-`x-private` operation on an included service gets its own
- **Named union aliases.** Every named TypeSpec `union` that is reachable from
an operation on an included service gets its own
`export type <Name> = <Variant1> | <Variant2> | …` in `types.ts` (`interface-types.ts`,
`unionVariantsType` in `ts-types.ts`) — variants resolve through the same
`RefName`/`refNameInput` machinery as model properties, so a model-variant is
Expand All @@ -328,18 +328,15 @@ Structural rules the interface emitter follows:
anything in the actual SDK surface referencing it — `computeReachableUnions` in
`emitter.tsx` walks every collected operation's request body, query
parameters, and response body (success and error) and only aliases unions it
reaches. Collected means non-`x-private`: `x-internal` operations count as
reachability roots because they are emitted under the `client.internal.*`
surface. `PriceUsageBased`, `ULIDOrResourceKey`, and
`ULIDOrExternalResourceKey` are declared but never referenced by anything, so
they stay zod-only (aliasing them would export a degenerate type like
`string | string`); `Invoice`/`InvoiceLine`/`UpdateInvoiceRequest` are
reachable only through the invoice operations marked `x-private`, which are
excluded from the generated client entirely, so their unions are excluded too
even though the underlying models (e.g. `InvoiceStandard`) still get
interfaces (models are never reachability-gated — only the new union alias
pass is). `Currency` is reachable through the `x-internal` currency
operations, so its alias is emitted. This is a deliberately narrower policy
reaches. Every operation counts as a reachability root: `x-internal` and
`x-private` operations are emitted under the `client.internal.*` surface, so
the unions they reach are aliased too (`Invoice`/`InvoiceLine`/
`UpdateInvoiceRequest` via the `x-private` invoice operations, `Currency` via
the `x-internal` currency operations). `PriceUsageBased`,
`ULIDOrResourceKey`, and `ULIDOrExternalResourceKey` are declared but never
referenced by anything, so they stay zod-only (aliasing them would export a
degenerate type like `string | string`); models are never reachability-gated
— only the union alias pass is. This is a deliberately narrower policy
than models', to avoid exporting unions nothing in the shipped client can
ever produce or accept.
- **Response wiring picks up named unions too.** Because a named union now
Expand Down Expand Up @@ -694,8 +691,8 @@ TypeSpec program in-memory, runs the emitter through the compiler's real emit
pipeline, and returns the emitted files as `outputs: Record<path, content>`
(paths relative to the emitter output dir, e.g. `src/sdk/internal.ts`). Use it
to pin generator behavior that should be caught before regenerating the real
client — `test/internal-surface.test.ts` (the x-private/x-internal
partitioning) is the model. Constraints:
client — `test/internal-surface.test.ts` (the x-private/x-internal routing to
the `client.internal.*` surface) is the model. Constraints:

- The tester resolves the emitter by its package name through `package.json`
exports, i.e. it runs the **built** `dist/` — the package `test` script runs
Expand Down
17 changes: 17 additions & 0 deletions api/spec/packages/aip-client-javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ TypeSpec definitions and ships fully-typed request and response models.
- [Defaults](#defaults)
- [Internal Operations](#internal-operations)
- [Internal Subscriptions](#internal-subscriptions)
- [Internal Invoices](#internal-invoices)
- [Internal Currencies](#internal-currencies)
- [Internal Governance](#internal-governance)
- [Runtime Validation (validate option)](#runtime-validation-validate-option)
- [Zod Schemas (./zod export)](#zod-schemas-zod-export)
- [Error Handling](#error-handling)
Expand Down Expand Up @@ -420,6 +422,15 @@ they can change or be removed without notice or semver consideration.
| ------------------------------------------- | ------------------------------------------------------- | ----------------------------- |
| `client.internal.subscriptions.createAddon` | `POST /openmeter/subscriptions/{subscriptionId}/addons` | Add add-on to a subscription. |

### Internal Invoices

| Method | HTTP | Description |
| --------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.internal.invoices.list` | `GET /openmeter/billing/invoices` | List billing invoices. Returns a page of invoices. Gathering invoices are never included. Use `filter` to narrow by status, customer, dates, or service period start. Use `sort` to control ordering. |
| `client.internal.invoices.get` | `GET /openmeter/billing/invoices/{invoiceId}` | Get a billing invoice by ID. Returns the full invoice resource including line items, status details, totals, and workflow configuration snapshot. |
| `client.internal.invoices.update` | `PUT /openmeter/billing/invoices/{invoiceId}` | Update a billing invoice. Only the mutable fields of the invoice can be edited: description, labels, supplier, customer, workflow settings, and top-level lines. Top-level lines are matched by `id`; lines without an `id` are created, and existing lines omitted from `lines` are deleted. Detailed (child) lines are always computed and cannot be edited directly. Only invoices in draft status can be updated. |
| `client.internal.invoices.delete` | `DELETE /openmeter/billing/invoices/{invoiceId}` | Delete a billing invoice. Only standard invoices in draft status can be deleted. Deleting an invoice will also delete all associated line items and workflow configuration. |

### Internal Currencies

| Method | HTTP | Description |
Expand All @@ -429,6 +440,12 @@ they can change or be removed without notice or semver consideration.
| `client.internal.currencies.listCostBases` | `GET /openmeter/currencies/custom/{currencyId}/cost-bases` | List cost bases for a currency. For custom currencies, there can be multiple cost bases with different `effective_from` dates. |
| `client.internal.currencies.createCostBasis` | `POST /openmeter/currencies/custom/{currencyId}/cost-bases` | Create a cost basis for a currency. |

### Internal Governance

| Method | HTTP | Description |
| ---------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.internal.governance.queryAccess` | `POST /openmeter/governance/query` | Query feature access for a list of customers. The endpoint resolves each provided identifier to a customer and returns the access status for the requested features, plus optional credit balance availability. _Designed to be called on a fixed refresh interval and the query response is intended to be cached._ |

## Runtime Validation (validate option)

`validate` is off by default. The SDK's normal request/response mapping
Expand Down
62 changes: 62 additions & 0 deletions api/spec/packages/aip-client-javascript/src/funcs/governance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Code generated by @openmeter/typespec-typescript. DO NOT EDIT.

import { type Client, http } from '../core.js'
import { type Result, type RequestOptions } from '../lib/types.js'
import { request } from '../lib/request.js'
import { toURLSearchParams, encodeSort } from '../lib/encodings.js'
import { toWire, fromWire, assertValid } from '../lib/wire.js'
import * as schemas from '../models/schemas.js'
import type {
QueryGovernanceAccessRequest,
QueryGovernanceAccessResponse,
} from '../models/operations/governance.js'

/**
* Query governance access
*
* Query feature access for a list of customers.
*
* The endpoint resolves each provided identifier to a customer and returns the
* access status for the requested features, plus optional credit balance
* availability.
*
* _Designed to be called on a fixed refresh interval and the query response is
* intended to be cached._
*
* POST /openmeter/governance/query
*/
export function queryGovernanceAccess(
client: Client,
req: QueryGovernanceAccessRequest,
options?: RequestOptions,
): Promise<Result<QueryGovernanceAccessResponse>> {
return request(() => {
const body = toWire(req.body, schemas.queryGovernanceAccessBody)
if (client._options.validate) {
assertValid(schemas.queryGovernanceAccessBodyWire, body)
}
const query = toWire(
{
page: req.page,
},
schemas.queryGovernanceAccessQueryParams,
)
if (client._options.validate) {
assertValid(schemas.queryGovernanceAccessQueryParamsWire, query)
}
const searchParams = toURLSearchParams(query)
return http(client)
.post('openmeter/governance/query', {
...options,
searchParams,
json: body,
})
.json()
.then((data) => {
if (client._options.validate) {
assertValid(schemas.queryGovernanceAccessResponseWire, data)
}
return fromWire(data, schemas.queryGovernanceAccessResponse)
})
})
}
2 changes: 2 additions & 0 deletions api/spec/packages/aip-client-javascript/src/funcs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './entitlements.js'
export * from './subscriptions.js'
export * from './apps.js'
export * from './billing.js'
export * from './invoices.js'
export * from './tax.js'
export * from './currencies.js'
export * from './features.js'
Expand All @@ -15,3 +16,4 @@ export * from './plans.js'
export * from './addons.js'
export * from './planAddons.js'
export * from './defaults.js'
export * from './governance.js'
Loading
Loading