From ff424c2c58aeeef8ee2b803921f3bdfcb715d962 Mon Sep 17 00:00:00 2001 From: GlacierLuo <1090490148@qq.com> Date: Fri, 16 Oct 2026 14:29:48 +0800 Subject: [PATCH 1/2] fix(actions): avoid collection routes when looking up schemas --- src/client.ts | 17 ++-- src/tests/client.action-detail.test.ts | 115 +++++++++++++++++++++++++ src/tests/workers-domain-bind.test.ts | 15 ++-- 3 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 src/tests/client.action-detail.test.ts diff --git a/src/client.ts b/src/client.ts index e1ce8a0..a69da51 100644 --- a/src/client.ts +++ b/src/client.ts @@ -762,12 +762,17 @@ export async function actionCategories(opts: ClientOptions, params: { source?: s } export async function actionGet(id: string, opts: ClientOptions) { - return request( - `${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`, - { method: 'GET', headers: headers(opts.apiKey) }, - DEFAULT_TIMEOUT_MS, - IDEMPOTENT_RETRIES, - ); + // Avoid fixed collection routes, including on backends without by-id lookup. + const result = await actionBatch([id], opts); + if (!result || !Array.isArray(result.actions) || !Array.isArray(result.missing_ids) + || !result.missing_ids.every((missingId) => typeof missingId === 'string') + || !result.actions.every((action) => action !== null && typeof action === 'object' && !Array.isArray(action))) { + throw new Error('Invalid Action batch response'); + } + if (result.missing_ids.includes(id) || result.actions.length === 0) { + throw new HttpError(404, `Action not found: ${id}`); + } + return result.actions; } export async function actionBatch(ids: string[], opts: ClientOptions) { diff --git a/src/tests/client.action-detail.test.ts b/src/tests/client.action-detail.test.ts new file mode 100644 index 0000000..55ff175 --- /dev/null +++ b/src/tests/client.action-detail.test.ts @@ -0,0 +1,115 @@ +import { afterEach, describe, expect, it, spyOn } from 'bun:test'; +import { actionCall, actionGet, HttpError } from '../client.ts'; + +describe('action detail lookup', () => { + let restoreFetch: (() => void) | undefined; + afterEach(() => restoreFetch?.()); + + it.each(['categories', 'search', 'services', 'batch', 'execute', 'solana', 'solana.root'])( + 'looks up %s through batch without hitting a collection route', + async (id) => { + const actions = [{ id, method: 'GET' }, { id, method: 'POST' }]; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + Response.json({ actions, missing_ids: [] }), + ); + restoreFetch = () => fetchSpy.mockRestore(); + expect(await actionGet(id, { actionHost: 'action.xapi.to', apiKey: 'test-key' })).toEqual(actions); + expect(fetchSpy).toHaveBeenCalledTimes(1); + const [url, options] = fetchSpy.mock.calls[0]!; + expect(String(url)).toBe('https://action.xapi.to/v1/actions/batch'); + expect(options?.method).toBe('POST'); + expect(JSON.parse(String(options?.body))).toEqual({ ids: [id] }); + expect(new Headers(options?.headers).get('XAPI-Key')).toBe('test-key'); + }, + ); + + it('preserves an exact UUID even when the returned human ID differs', async () => { + const uuid = 'c8363ff2-8bfb-4ce0-a64a-3b7e8e56706a'; + const actions = [{ id: 'solana.root', uuid, method: 'POST', endpoint: { path: '/root' } }]; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + Response.json({ actions, missing_ids: [] }), + ); + restoreFetch = () => fetchSpy.mockRestore(); + expect(await actionGet(uuid, { actionHost: 'action.xapi.to' })).toEqual(actions); + expect(JSON.parse(String(fetchSpy.mock.calls[0]![1]?.body))).toEqual({ ids: [uuid] }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('preserves root methods and distinct schemas', async () => { + const actions = [ + { id: 'solana', method: 'GET', input: { type: 'object', properties: { query: {} } } }, + { id: 'solana', method: 'POST', input: { type: 'object', properties: { body: {} } } }, + ]; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + Response.json({ actions, missing_ids: [] }), + ); + restoreFetch = () => fetchSpy.mockRestore(); + expect(await actionGet('solana', { actionHost: 'action.xapi.to' })).toEqual(actions); + }); + + it('forwards the discovered UUID and selected method unchanged to execute', async () => { + const uuid = 'c8363ff2-8bfb-4ce0-a64a-3b7e8e56706a'; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue(Response.json({ success: true })); + restoreFetch = () => fetchSpy.mockRestore(); + await actionCall(uuid, { body: { value: 1 } }, { actionHost: 'action.xapi.to' }, 'POST'); + const [url, options] = fetchSpy.mock.calls[0]!; + expect(String(url)).toBe('https://action.xapi.to/v1/actions/execute'); + expect(JSON.parse(String(options?.body))).toEqual({ + action_id: uuid, method: 'POST', input: { body: { value: 1 } }, + }); + }); + + it('preserves not-found semantics for a missing ID', async () => { + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + Response.json({ actions: [], missing_ids: ['missing'] }), + ); + restoreFetch = () => fetchSpy.mockRestore(); + const error = await actionGet('missing', { actionHost: 'action.xapi.to' }).catch((error: unknown) => error); + expect(error).toBeInstanceOf(HttpError); + expect((error as HttpError).status).toBe(404); + }); + + it('does not replace a stale UUID with a root alias', async () => { + const uuid = 'c8363ff2-8bfb-4ce0-a64a-3b7e8e56706a'; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + Response.json({ actions: [], missing_ids: [uuid] }), + ); + restoreFetch = () => fetchSpy.mockRestore(); + await expect(actionGet(uuid, { actionHost: 'action.xapi.to' })).rejects.toThrow('HTTP 404'); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('preserves not-found semantics for an empty success response', async () => { + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + Response.json({ actions: [], missing_ids: [] }), + ); + restoreFetch = () => fetchSpy.mockRestore(); + await expect(actionGet('missing', { actionHost: 'action.xapi.to' })).rejects.toThrow('HTTP 404'); + }); + + it.each([401, 403, 404])('propagates HTTP %s without trying a legacy route', async (status) => { + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + Response.json({ message: 'Access denied or unavailable' }, { status }), + ); + restoreFetch = () => fetchSpy.mockRestore(); + const error = await actionGet('categories', { actionHost: 'action.xapi.to' }).catch((error: unknown) => error); + expect(error).toBeInstanceOf(HttpError); + expect((error as HttpError).status).toBe(status); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it.each([ + null, + { success: false, error: { code: 'FORBIDDEN' } }, + { data: { actions: [], missing_ids: [] } }, + { actions: [], missing_ids: 'missing' }, + { actions: [], missing_ids: [null] }, + { actions: null, missing_ids: [] }, + { actions: [null], missing_ids: [] }, + ])('rejects an invalid success envelope: %j', async (payload) => { + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue(Response.json(payload)); + restoreFetch = () => fetchSpy.mockRestore(); + await expect(actionGet('solana', { actionHost: 'action.xapi.to' })).rejects.toThrow('Invalid Action batch response'); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/tests/workers-domain-bind.test.ts b/src/tests/workers-domain-bind.test.ts index 921287c..a6c9e5d 100644 --- a/src/tests/workers-domain-bind.test.ts +++ b/src/tests/workers-domain-bind.test.ts @@ -13,12 +13,10 @@ describe("xdomain + Workers binding", () => { (async (target: string | URL | Request, init?: RequestInit) => { const url = String(target); calls.push(url); - if (url.includes("/v1/actions/") && (!init?.method || init.method === "GET")) { - actionIds.push(decodeURIComponent(url.split("/").pop() || "")); - return new Response(JSON.stringify([{ id: actionIds.at(-1) }]), { - status: 200, - headers: { "content-type": "application/json" }, - }); + if (url.endsWith("/v1/actions/batch") && init?.method === "POST") { + const { ids } = JSON.parse(String(init.body)) as { ids: string[] }; + actionIds.push(...ids); + return Response.json({ actions: ids.map((id) => ({ id })), missing_ids: [] }); } if (url.endsWith("/v1/actions/execute")) { const body = JSON.parse(String(init?.body)); @@ -96,8 +94,9 @@ describe("xdomain + Workers binding", () => { fetchSpy = spyOn(globalThis, "fetch").mockImplementation( (async (target: string | URL | Request, init?: RequestInit) => { const url = String(target); - if (url.includes("/v1/actions/") && (!init?.method || init.method === "GET")) { - return Response.json([{ id: decodeURIComponent(url.split("/").pop() || "") }]); + if (url.endsWith("/v1/actions/batch") && init?.method === "POST") { + const { ids } = JSON.parse(String(init.body)) as { ids: string[] }; + return Response.json({ actions: ids.map((id) => ({ id })), missing_ids: [] }); } if (url.endsWith("/v1/actions/execute")) { const body = JSON.parse(String(init?.body)); From 3ab01af04493004117fd966c39206acc0716743e Mon Sep 17 00:00:00 2001 From: GlacierLuo <1090490148@qq.com> Date: Fri, 16 Oct 2026 14:29:49 +0800 Subject: [PATCH 2/2] fix(skill): align Decisions HTTP2 and Aptos provider workflows --- README.md | 35 ++++- skills/xapi-provider/SKILL.md | 21 ++- .../reference/ai-spec-and-models.md | 119 +++++++++++---- .../xapi-provider/reference/billing-modes.md | 65 ++++++++- .../xapi-provider/reference/http-transport.md | 102 +++++++++++++ .../reference/lifecycle-management.md | 69 ++++++++- .../reference/validation-checklist.md | 18 ++- skills/xapi/SKILL.md | 30 +++- skills/xapi/guides/ai.md | 2 +- skills/xapi/guides/ai_gateway.md | 112 +++++++++++++- skills/xapi/guides/blockpi.md | 137 +++++++++++++++++- skills/xapi/guides/provider.md | 40 ++++- src/commands/provider-onboarding.ts | 4 +- src/tests/skill-live-services-guide.test.ts | 2 +- 14 files changed, 688 insertions(+), 68 deletions(-) create mode 100644 skills/xapi-provider/reference/http-transport.md diff --git a/README.md b/README.md index 32114c4..96a28f6 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,24 @@ one request object or a mini-batch array; `serper.reviews` accepts only one object. See the bundled [Serper guide](skills/xapi/guides/serper.md) for the Action list, examples, and dynamic per-credit billing. +For API Actions, `/` uses the bare service ID (`solana`), while literal `/root` +uses `solana.root`; `.root` is not a root alias. Use exact discovered IDs and +endpoint UUIDs without rewriting them. A replacement serving revision can +invalidate old UUIDs, so rediscover and inspect the new schema on 404 rather +than falling back to a service alias. Multiple HTTP methods still require +`--method` when selecting an endpoint. + +Single-schema lookup uses `POST /v1/actions/batch` with one ID, retaining +compatibility with backends without the dedicated `GET /v1/actions/by-id/{id}` +route. The legacy dynamic GET can collide with collection names such as +`search`, `categories` and `services`. + +For typed Decisions (`state/questions/answers`, no SSE or chat translation), +use the [AI Gateway guide](skills/xapi/guides/ai_gateway.md). For dedicated +non-EVM RPC or Aptos JSON/BCS, use the [BlockPI guide](skills/xapi/guides/blockpi.md). +`call --output` saves a binary response; raw BCS request input currently needs +the service HTTP Gateway, not JSON Action execution. + Search uses `--sort default|relevance|price`. `default` is the recommended order: it considers keyword coverage and match quality first, then favors stable built-in capabilities when matches are otherwise comparable. @@ -211,8 +229,9 @@ Saving that field alone does not re-import endpoint definitions; configure - `--mode merge` (default) sends PATCH and preserves omitted fields/endpoints. Existing endpoint edits require `id`, e.g. `{"endpoints":[{"id":"ENDPOINT_ID","costPerCall":"0.002"}]}`. -- `--allow-new-endpoints` explicitly permits ID-less merge entries to create - endpoints. Repeating such a merge can create duplicates. +- `--allow-new-endpoints` permits ID-less merge entries past the CLI guard; + backend matching by stable `endpointKey` or derived method/path still applies. + Prefer IDs from the target working revision for ordinary updates. - `--mode replace` sends PUT. If `endpoints` is provided, it replaces the endpoint list; include every endpoint you intend to keep. Omitted fields otherwise follow backend PUT semantics. Use full configuration for replacement. @@ -222,6 +241,9 @@ configuration returned by the backend. Find endpoint IDs in `currentVersion.endpoints`; `provider versions` returns working revision IDs in `majors[].working.id`. For an already-published API, use the existing `provider revision start ` command to create a working revision. +Cloning creates new endpoint UUIDs and preserves `endpointKey`; re-read the +working contract after cloning. Use `majors[].servingRevisionId` to identify +the serving revision. A working revision in `IN_REVIEW` cannot be edited. `provider update` without `--revision` continues to update service metadata and rate limits. Existing `version update`, `publish`, and positional `review` commands remain available. The `submit` and `review --revision` forms are @@ -234,7 +256,8 @@ rebuilds the derived authentication configuration. An empty map without an explicit `authConfig` clears those credentials. Omitting both fields preserves them. `submit` returns `{serviceId, revisionId, submission}`. A successful submission -does not guarantee publication. `wait` checks the requested revision, succeeds +does not guarantee publication, but a passing review can automatically publish +and switch live traffic. `wait` checks the requested revision, succeeds only for `PUBLISHED`, and outputs the review report with `success` and `reason`. `--changelog` is limited to 2,000 characters by both the CLI and backend. Rejection, a draft/sandbox/suspended revision, or a legacy manual-review hold @@ -251,6 +274,12 @@ to inspect the result before repeating the operation. No npm release is implied by a local source checkout; use `bun run src/index.ts provider ...` or build and run `node dist/index.js provider ...` while testing unreleased changes. +Use the [AI provider reference](skills/xapi-provider/reference/ai-spec-and-models.md) +for Decisions and DYNAMIC upstream-cost billing, and +[HTTP transport](skills/xapi-provider/reference/http-transport.md) for HTTP/2 +OpenAPI metadata, HTTPS/ALPN and update/clear behavior. Import and version update +already pass these fields; no HTTP/2 CLI client or flag is required. + ### Sandbox Commands Sandbox commands provide an AI-friendly cloud computer lifecycle. The fastest diff --git a/skills/xapi-provider/SKILL.md b/skills/xapi-provider/SKILL.md index c90cd20..898b742 100644 --- a/skills/xapi-provider/SKILL.md +++ b/skills/xapi-provider/SKILL.md @@ -44,8 +44,11 @@ scope. Do not try unrelated keys. - Fixed, token, resource, or zero-price billing: read [billing](reference/billing-modes.md) and [free access](reference/free-api-access.md). -- AI models or token accounting: read +- AI models, typed Decisions, token accounting or upstream-cost billing: read [AI services](reference/ai-spec-and-models.md). +- HTTP/2 upstreams or transport metadata in OpenAPI contracts: read + [HTTP transport](reference/http-transport.md). CLI import/update already + passes the configuration; no HTTP/2 CLI flag is needed. - WebSocket endpoints: read [WebSocket endpoints](reference/websocket-endpoints.md). - OAuth user-context endpoints: read @@ -67,7 +70,9 @@ npx xapi-to provider versions ``` Use the returned version ID for contract changes. PATCH merges supplied fields; -`--replace` sends a full replacement: +`--replace` sends a full replacement. Preserve each endpoint's `endpointKey` +across working-revision clones and use the working endpoint ID for partial +patches; see the lifecycle reference for exact matching rules: ```bash npx xapi-to provider version update \ @@ -76,7 +81,9 @@ npx xapi-to provider diff ``` Submit only after inspecting the diff. Publishing can change live behavior and -the CLI deliberately does not retry an ambiguous write: +the CLI deliberately does not retry an ambiguous write. A passing review can +automatically publish and switch the serving revision; submission is not a +dry run: ```bash npx xapi-to provider publish \ @@ -119,9 +126,13 @@ npx xapi-to provider skill context ``` The scaffold command refuses to overwrite an existing file unless `--force` is -explicit. Link only a Skill owned by the same provider. If `skill context` +explicit. Check its public URL, HTTP method, body and media types against the +serving contract before submitting; generated text can require correction. +Link only a Skill owned by the same provider. If `skill context` reports contract drift, update and resubmit the Skill before recording a new -fingerprint. Use `provider skill unlink ` to remove the association. +fingerprint. `drift: false` does not prove schemas, media types or transport +are unchanged; the current fingerprint tracks host, major and endpoint keys. +Use `provider skill unlink ` to remove the association. ## Observe, recover, and earn diff --git a/skills/xapi-provider/reference/ai-spec-and-models.md b/skills/xapi-provider/reference/ai-spec-and-models.md index 60b2b20..8ae5a03 100644 --- a/skills/xapi-provider/reference/ai-spec-and-models.md +++ b/skills/xapi-provider/reference/ai-spec-and-models.md @@ -1,48 +1,109 @@ -# AI services and model pricing +# AI interfaces, models and pricing -Create AI services with the same provider CLI workflow as other services. The -endpoint path helps xAPI identify standard interfaces such as OpenAI chat, -Responses, embeddings, images, Claude messages, and Gemini. Keep standard paths -when the upstream is compatible; non-standard paths usually require explicit -`tokenJsonPaths`. +Choose protocol and billing mode independently. Standard paths identify the +wire protocol; they do not choose prices or turn DYNAMIC into PER_TOKEN. +For outbound transport, read [HTTP transport](http-transport.md). -Before writing the contract, establish: +| Interface | Standard paths | Token usage preset | +|---|---|---| +| OpenAI chat | `/v1/chat/completions` | `usage.prompt_tokens`, `usage.completion_tokens` | +| Claude | `/v1/messages`, `/messages` | `usage.input_tokens`, `usage.output_tokens` | +| Responses | `/v1/responses`, `/responses` | `usage.input_tokens`, `usage.output_tokens` | +| Embeddings | `/v1/embeddings`, `/embeddings` | `usage.prompt_tokens` | +| Decisions | `/v1/decisions`, `/decisions`, stored `{{alpha}}/decisions` | `usage.input_tokens`, `usage.output_tokens` | -1. the upstream public base URL and authentication type; -2. the supported model names; -3. input and output USD price per token for each model; -4. the largest allowed request/output so `estimatedMaxTokens` is realistic; -5. whether streaming responses contain final usage. +The backend computes detection and raw-to-Canonical mappings. Do not invent a +Canonical ID or submit `aiSpecDetectedBy: "path"` as proof of trust. Inspect +the saved association before publication. Shared Gateway routing needs a +compatible, trusted, public serving endpoint and a resolved Canonical mapping. -Use [`templates/ai-model.json`](../templates/ai-model.json) as a starting point: +## Declare models for the billing mode -```bash -npx xapi-to provider create --file ./ai-service.json -npx xapi-to provider versions -``` +- **PER_TOKEN:** model keys in `tokenPricing` declare models and rates; + `servedModels` does not replace pricing. Obtain approved USD-per-token rates + from the user or authoritative upstream source. A `default` rate is a pricing + fallback, not proof that every unknown model is supported; use an agreed rate. +- **DYNAMIC / PER_CALL / PER_RESOURCE AI:** declare upstream names in + `servedModels`; the backend derives `aiModelMappings`. DYNAMIC charges come + from `dynamicRules`, not `tokenPricing`. Drafts can be incomplete; submission + enforces the applicable served-model requirement. + +Scoped `provider create` and `provider version update` accept `servedModels` +on endpoint DTOs. The one-shot `provider import` OpenAPI flow has no served-model +extension; configure the draft afterward with a scoped endpoint payload. +See [billing modes](billing-modes.md). + +Use [the AI template](../templates/ai-model.json) for PER_TOKEN chat. Replace +illustrative models and prices before creation. Tiered, cached, reasoning or +multimodal rates must follow the supported pricing contract; do not flatten them +without the user's agreement. + +Recognized protocols use built-in token presets. Non-standard paths or usage +need explicit `tokenJsonPaths`. For SSE/NDJSON, verify final usage extraction; +missing usage can settle against the configured estimate. Decisions has no SSE. + +## Decisions provider configuration + +Decisions uses `model`, `state` and named `questions`, returning typed `answers`. +It is separate from chat, with no chat translation or streaming. Consumer +examples and discovery are in the [Gateway guide](../../xapi/guides/ai_gateway.md#decisions). -For multi-model services, include a `default` price and optional -`servedModels`. Do not invent pricing. If the upstream has tiered, cached, -reasoning, audio, image, or tool-call pricing, represent it in `tokenPricing` -instead of flattening it without the user's agreement. +For OpenRouter, preserve stored `{{alpha}}/decisions` and named alpha base +`https://openrouter.ai/api/alpha`. Public consumers use `/decisions`. +Replacing the stored path loses named-base selection. For cost passthrough, +retain the existing `usage.cost` rule, multiplier, hold estimate and caps; +do not add token prices merely because usage includes token counts. -For SSE, request upstream usage in the terminal event when its protocol -supports that option. For NDJSON, place usage in the last JSON line. If final -usage cannot be extracted, the platform may charge the configured maximum. +For a model-only PATCH, take the endpoint ID from the working copy: -Use a version patch for model or pricing updates: +```json +{ + "endpoints": [{ + "id": "", + "servedModels": ["typesafe/jev-1.14"] + }] +} +``` ```bash npx xapi-to provider revision start npx xapi-to provider version update \ - --file ./models.patch.json + --file ./models.patch.json npx xapi-to provider diff -npx xapi-to provider publish \ - --changelog "Update model catalog and pricing" ``` -After a canary call, use its request ID to wait for finalized accounting: +This replaces that endpoint's declared model list. Preserve all models that +should remain supported. PATCH leaves omitted fields intact; do not use +`--replace` for this partial file. Submission is separate and may automatically +publish after review; follow [lifecycle management](lifecycle-management.md). +Check that the existing Decisions configuration has `supportsStreaming: false`; +this model-only patch does not change it or other billing/transport fields. + +## Variants, reference prices and receipts + +Canonical identity, upstream name and wire protocol are separate: `jev-1-14` +can map to `typesafe/jev-1.14` on a Decisions endpoint. A moving `latest` +identity must not be silently pinned to a fixed version. + +Preserve complete variants. OpenRouter `:free` needs a free mapping and must +never fall back to the paid base. `:batch` is unsupported by the synchronous +Decisions Gateway. Discover current models rather than copying a fixed count. + +Eligible serving OpenRouter Decisions endpoints may expose response-only +`modelQuotes`: USD-per-million-token estimates derived from Canonical reference +prices and this endpoint's simple cost-passthrough multiplier. These are not +`tokenPricing` and do not settle DYNAMIC calls. Other variants cannot borrow +base rates. Explicit free quotes are zero only when the rule can be safely +quoted; complex rules, including caps/clamps, remain unknown. Missing/null +means unknown; `0` means an explicit zero rate. Keep billing limits intact. + +After an authorized canary, verify actual cost, model/Canonical attribution and +final accounting. Where the response exposes an xAPI receipt request ID: ```bash npx xapi-to usage wait --timeout 1m ``` + +The AI Gateway currently does not expose a reliable receipt request ID. Its +`X-XAPI-Cost` aggregates attempts; the final provider's `usage.cost` or response +`id` is not an aggregate xAPI receipt. Do not claim reconciliation from them. diff --git a/skills/xapi-provider/reference/billing-modes.md b/skills/xapi-provider/reference/billing-modes.md index 03258e9..7b441e4 100644 --- a/skills/xapi-provider/reference/billing-modes.md +++ b/skills/xapi-provider/reference/billing-modes.md @@ -42,8 +42,9 @@ valid; read [free access](free-api-access.md) before publishing a free endpoint. } ``` -Always obtain pricing from the user or an authoritative upstream source. Keep a -`default` entry for unknown model names. Standard AI paths are detected by the +Always obtain pricing from the user or an authoritative upstream source. Use an +agreed `default` rate if a pricing fallback is needed; it does not declare every +unknown model routable. Standard AI paths are detected by the platform; use `tokenJsonPaths` only for a non-standard request or response: ```json @@ -79,6 +80,66 @@ The count path may resolve to an array, whose length is charged, or a numeric count. For streamed NDJSON/SSE, the final structured chunk must expose the count. The estimated maximum is the pre-charge cap. +## Response-derived rules: `DYNAMIC` + +Use DYNAMIC for a response-cost rule or other supported field-based pricing. +AI protocols and token counts do not force PER_TOKEN. A DYNAMIC AI endpoint +declares `servedModels`; its rates and hold estimate live in `dynamicRules`. +The backend derives the protocol/model association from the saved endpoint. + +This illustrative Decisions endpoint settles upstream `usage.cost × 1.2`: + +```json +{ + "name": "Decisions", + "method": "POST", + "path": "/v1/decisions", + "billingType": "DYNAMIC", + "servedModels": ["typesafe/jev-1.14"], + "supportsStreaming": false, + "dynamicRules": { + "version": 2, + "fields": [{ + "name": "cost", "source": "response.body", "path": "usage.cost", + "type": "number", "estimatedMax": 0.1, "clamp": {"min": 0} + }], + "rules": [{ + "then": {"operands": [{"factors": [ + {"kind": "number", "field": "cost"}, + {"kind": "const", "value": 1.2} + ]}]} + }], + "minCost": 0, + "maxCost": 10 + } +} +``` + +Choose the actual multiplier, estimate and caps with the user; these numbers +are not a universal fee schedule. The estimate can hold funds before the real +response arrives (here 0.1 × 1.2 = USD 0.12). Final consumption follows the +configured rules and actual billing evidence. Missing/invalid evidence is not +proof of zero cost; inspect finalized accounting instead of calculating a refund +from missing usage. Preserve existing rules on a model-only update. + +For OpenRouter's existing endpoint, preserve `{{alpha}}/decisions` and its +named alpha base rather than copying this example's path. Decisions is a +single non-streaming request; do not enable SSE or `:batch` to test this rule. + +Reference quotes are display-only. A simple recognized passthrough can derive +USD/1M-token estimates; rules with caps/clamps, including this example, remain +unknown. Unknown/null is distinct from zero. OpenRouter `:free` must retain the +exact free mapping; never borrow the paid base's price or use it as fallback. +See [AI models](ai-spec-and-models.md) for variants and quote boundaries. + +## Accounting after execution + +Estimates/reservations are not actual consumption. Check the actual USD cost, +finalized receipt where an xAPI request ID is available, and released holds. +An upstream response ID is not an xAPI receipt ID. Gateway costs aggregate +attempts, so final upstream `usage.cost` or a single attempt receipt cannot +reconcile the whole request. See the [Gateway billing headers](../../xapi/guides/ai_gateway.md#routing-and-billing-headers). + After changing prices, inspect the diff before publication: ```bash diff --git a/skills/xapi-provider/reference/http-transport.md b/skills/xapi-provider/reference/http-transport.md new file mode 100644 index 0000000..ba2a2ac --- /dev/null +++ b/skills/xapi-provider/reference/http-transport.md @@ -0,0 +1,102 @@ +# HTTP upstream transport + +Read this when an upstream requires HTTP/2 or when importing/editing an existing +transport choice. This is separate from request media type, AI protocol and billing. + +## Backend revision contract + +Store a numeric selector under OpenAPI `info`: + +```json +{ + "openapi": "3.0.3", + "info": { + "title": "Example RPC", + "version": "1.0.0", + "x-xapi-upstream-http-version": 2 + }, + "servers": [{"url": "https://rpc.example.com"}], + "paths": {} +} +``` + +This shows transport metadata; complete the real operations before submission. +Scoped create/version DTOs carry it in `openApiSpec`, either as the raw contract +above or wrapped as `{"spec": }`. There is no endpoint-level +or top-level DTO `upstreamHttpVersion` field to send instead. + +| Value | Backend behavior | +|---|---| +| Omitted or numeric `1` | Existing HTTP/1.1 transport | +| Numeric `2` | HTTPS, verified TLS, ALPN `h2`; no HTTP/1.1 downgrade | +| String `"2"`, null extension value, other numbers | Invalid selector | + +Every configured non-empty default/named base must use HTTPS for version 2. +An incomplete draft can retain the selector before a base is configured; +submission requires a complete public upstream. Named-base targets are checked +again at runtime. SSRF, blocked-address/port and DNS-rebinding protection remain +enabled. Provider Host overrides affect HTTP/2 `:authority`, not connection +destination, URL-based TLS identity or SNI. + +Synchronous calls and supported async submit/poll/cancel paths use the selector. +Queued tasks freeze it at submission; a revision change does not rewrite an +existing task. HTTP/2 transport does not automatically retry or follow redirects. + +## Preserve, replace or clear + +- A new major inherits source `openApiSpec` when the field is omitted. +- Metadata-only updates omitting `openApiSpec` retain the stored contract. +- Explicit raw or wrapped `openApiSpec` replaces that entire field, even in + PATCH. Merge the intended info change into the existing contract first and + preserve other fields. Endpoint PATCH merge does not deep-merge the spec. +- `{"openApiSpec": null}` clears the contract and restores the HTTP/1.1 default. + This differs from an invalid null selector inside `info`. +- Form imports with a selector merge it into existing transport info. Imports + without one leave an existing choice alone; new services default to HTTP/1.1. + Legacy imports retain transport metadata beside editable endpoint rows rather + than freezing the original paths map. +- Previously lost selectors require an explicit revision update; their missing + intent cannot be inferred. + +Use a working revision. Put the complete updated `openApiSpec` in the patch: + +```bash +npx xapi-to provider revision start +npx xapi-to provider version update \ + --file ./transport.patch.json +npx xapi-to provider diff +``` + +Protocol changes require connectivity review through normal publication. +The editor's `/agent/test` and `test-config` currently do not propagate this +selector; their success does not prove HTTP/2 connectivity. Verify a real TLS +h2-only target through the configured runtime and check normal accounting before +claiming end-to-end support. + +## xhub is a separate connection + +An xhub platform manifest uses a **string**: + +```json +{ + "upstream": "https://provider.example.com", + "upstreamHttpVersion": "2" +} +``` + +This is a fragment of `platforms//manifest.json`, not a backend DTO. +Complete the platform's other required fields. Omitted keeps existing fetch +behavior; `"1.1"` explicitly pins HTTP/1.1; `"2"` requires HTTPS/ALPN h2 and +Bun >= 1.4.3. Check the deployed xhub runtime before enabling it. + +```text +Consumer → backend → HTTP/1.1 xhub → HTTPS HTTP/2 provider +``` + +When xhub accepts HTTP/1.1, leave the backend selector omitted. Enable xhub's +manifest independently for its onward connection. Backend HTTP/2 support alone +does not configure or publish an xhub platform. + +These settings do not implement gRPC/Protobuf framing or `grpc-status` handling, +cleartext h2c, WebSockets or consumer ingress HTTP/2. Verify the actual upstream +protocol before treating an HTTP/2 selector as sufficient. diff --git a/skills/xapi-provider/reference/lifecycle-management.md b/skills/xapi-provider/reference/lifecycle-management.md index 1b27a12..9191813 100644 --- a/skills/xapi-provider/reference/lifecycle-management.md +++ b/skills/xapi-provider/reference/lifecycle-management.md @@ -22,7 +22,31 @@ npx xapi-to provider diff ``` Do not infer version IDs from version labels. Use the IDs returned by `create`, -`versions`, or `revision start`. +`versions`, or `revision start`. In `versions`, each `majors[]` entry has a +`servingRevisionId`, an optional `working.id/state`, and published `history`. +Read the working contract rather than editing the currently serving revision. +For a currently serving published revision, verify `state: "PUBLISHED"` and +`isServing: true`, with the service active and major not deprecated. A historical +published revision is not necessarily serving. The legacy revision `status` +field is not the routing truth. + +`revision start` may return an existing working revision, including one already +in review. Only `DRAFT` and `SANDBOX` are editable; inspect state before writing. + +Use the major and working `revision` number from the overview to read its +contract, for example v1.4 for major 1, revision 4: + +```bash +npx xapi-to provider get --version v1.4 +``` + +Verify the returned revision ID equals the intended working ID before taking +endpoint IDs from it. Do not blindly resubmit the redacted service response as +a full configuration file. + +Provider CLI reads and writes use the service owner's scoped key. An owner +preview does not validate a different administrator's visibility or permissions; +verify each actual role separately when it is part of the requested acceptance. ## Create payload @@ -124,9 +148,23 @@ npx xapi-to provider version update \ --file ./contract.patch.json ``` -Include an endpoint `id` when updating an existing endpoint. Omitting it means -new endpoint. Use `--replace` only with a complete version contract; it sends a -PUT and replaces omitted contract data: +Prefer the endpoint `id` from the target working revision for partial updates. +Copy-on-write creates new endpoint UUIDs but preserves `endpointKey`. Preserve +that key when carrying an endpoint across revisions. The current backend matches +an endpoint by a current `id`, then by `endpointKey`; when no key is supplied, +a complete `method` + `path` may match its derived key. A stale ID can therefore +match a preserved endpointKey. Conflicting supplied IDs and keys are rejected. +Do not assume that omitting `id` always creates an endpoint, or that an endpoint +UUID remains stable across revisions. + +An unmatched explicit ID in PATCH is rejected rather than silently creating a +row. To add an endpoint, omit its ID and supply its name, method and path; ensure +it does not match an existing stable or derived key. Editing does not itself +increment the revision label. + +PATCH keeps omitted endpoints. PUT with an `endpoints` list removes unmatched +endpoints; include every endpoint to retain, with its identity and complete +configuration. Use `--replace` only with a complete version contract: ```bash npx xapi-to provider version update \ @@ -141,6 +179,23 @@ npx xapi-to provider revision start npx xapi-to provider major create ``` +Read the new revision and its endpoint IDs after cloning. The onboarding alias +`provider update --revision --file ... --mode merge` has an +additional CLI guard requiring endpoint IDs unless `--allow-new-endpoints` is +explicitly supplied. That flag permits ID-less entries; it does not override +backend identity matching. Use working IDs for routine patches rather than +enabling it merely to bypass the guard. + +Transport metadata is part of `openApiSpec`, which is replaced as a whole when +explicitly supplied, even in PATCH. See [HTTP transport](http-transport.md) +before changing or clearing HTTP/2 configuration. + +Keep the editable endpoint rows and any operations in a stored +`openApiSpec.paths` consistent. Updating `openApiSpec` does not re-import the +endpoint list. Named-base selection remains part of the stored endpoint path: +use `{{alpha}}/new` to select alpha, or `/new` for the default base. The selector +is omitted only from the public Gateway path. + ## Publish and review Before publishing, the revision needs a valid public upstream and at least one @@ -158,6 +213,12 @@ The changelog is public provider-authored release information. Do not put credentials, internal logs, or review-worker output in it. If publish returns an ambiguous transport failure, read state before deciding whether to submit again. +Submission can automatically publish after a passing review and switch live +traffic to the new revision. Obtain authorization for that effect before +submitting. Check `review`, then `versions` and `servingRevisionId` to verify the +actual result; HTTP success or a `PUBLISHED` historical revision alone is not +proof of current serving state. + ## Rollback, major routing, and removal Rollback targets an existing published revision in one major: diff --git a/skills/xapi-provider/reference/validation-checklist.md b/skills/xapi-provider/reference/validation-checklist.md index 6e2e203..b11c0c1 100644 --- a/skills/xapi-provider/reference/validation-checklist.md +++ b/skills/xapi-provider/reference/validation-checklist.md @@ -20,12 +20,26 @@ before `provider version update --replace`, and again before publication. ## Endpoints - Every new endpoint has `name`, `method`, and `path`. -- Existing endpoint patches include `id`; otherwise the backend treats the item - as new. +- Existing endpoint patches use the ID from the working revision. The backend + can also match a stable method/path through its derived endpoint key; missing + `id` does not always mean new. Preserve the stored `endpointKey` across + revisions and use the working ID when changing a path; inspect the diff for + unintended additions or removals. - Path parameters use `:name` and have matching `pathParams` descriptions. - Billing fields match the chosen `billingType`; pre-charge estimates are realistic. - Streaming endpoints set both `supportsStreaming` and `streamFormat`. +- AI billing and protocol are chosen independently: PER_TOKEN uses model keys + in `tokenPricing`; DYNAMIC/PER_CALL/PER_RESOURCE AI uses `servedModels`. + Confirm saved Canonical mappings and detection rather than claiming trust + with provider-supplied `aiSpecDetectedBy`. +- Decisions retains `state` and named typed `questions`, sets + `supportsStreaming: false`, and preserves DYNAMIC rules and model variants. + `:free` must not route to a paid base; `:batch` is not synchronous Decisions. +- HTTP/2 uses numeric `info.x-xapi-upstream-http-version: 2` in `openApiSpec`, + HTTPS default/named bases and a compatible deployed runtime. Review whole-spec + replacement/clear semantics and the independent xhub manifest string option + in [HTTP transport](http-transport.md). - WebSocket endpoints set `protocol: "WEBSOCKET"`, a supported adapter, a public `wsConfig.upstreamUrl`, and a valid `wsBilling` object. - OAuth user-context endpoints retain their approved `userOAuthProviderId`. diff --git a/skills/xapi/SKILL.md b/skills/xapi/SKILL.md index ed9b9d1..b394909 100644 --- a/skills/xapi/SKILL.md +++ b/skills/xapi/SKILL.md @@ -111,6 +111,22 @@ Ranking is global: the service sorts all matching results before applying `--pag `get-batch` accepts at most 100 action IDs per invocation. +### Action identifiers and detail lookup + +For third-party APIs, `/` uses the bare service ID (`solana`); the literal +`/root` uses `solana.root`. `.root` is not a root alias. Use the exact discovered +ID for `get` and `call`, including endpoint UUIDs. UUID lookup identifies the +endpoint only; it never falls back to a service with the same name. A service +whose name is UUID-shaped exposes its root by endpoint UUID. + +UUIDs belong to a revision. After a replacement revision becomes serving, an +old UUID can return 404: rediscover the endpoint and inspect its new schema +before calling. Do not substitute `.root` or replay an uncertain write. +For direct HTTP detail reads, use `GET /v1/actions/by-id/{id}` or +`POST /v1/actions/batch` with `{"ids":[""]}`; inspect `actions` and +`missing_ids`. The legacy dynamic GET can collide with collection names such +as `search`, `categories`, and `services`. + ### Calling APIs ```bash @@ -176,7 +192,7 @@ Use built-in `crypto.*` for normalized multi-provider market, token, wallet, DEX, CEX, and news data. Use a specialized service instead when the user asks for its provider-native behavior: -- BlockPI network RPC or an arbitrary EVM JSON-RPC method → `guides/blockpi.md`. +- BlockPI EVM, Solana, NEAR, Starknet, Bitcoin RPC or Aptos REST/BCS → `guides/blockpi.md`. - Binance-native chain IDs, address analytics, RWA, aggregation, transaction, wallet, or DeFi data → `guides/binance_web3.md`. @@ -204,7 +220,9 @@ npx xapi-to task wait --interval 2s --timeout 10m ``` For application integrations, read `guides/ai_gateway.md` before configuring an -Anthropic/OpenAI-compatible client. Read `guides/ws_gateway.md` before opening a +Anthropic/OpenAI-compatible client or using the separate typed Decisions +protocol (`state`/`questions`/`answers`, no SSE or chat translation). Read +`guides/ws_gateway.md` before opening a persistent GPT Live, Realtime, ASR, TTS, interpretation, or podcast WebSocket session. GPT Live uses `/v1/live/sessions` and is not the Realtime protocol at `/v1/realtime`. @@ -299,8 +317,8 @@ Beyond built-in capabilities, xapi proxies **dozens** of third-party API service - **Weibo** (`weibo-app`) — Weibo API (user profiles, feeds, search, trending) - **5SIM SMS** (`5sim-sms`) — SMS verification (virtual numbers, activation codes) - **Serper API** (`serper`) — 12 provider-native Google Search actions including web, images, news, maps, places, video, shopping, scholar, patents, autocomplete, Lens, and reviews. Eleven support mini-batch; Reviews does not. Read `guides/serper.md` before calling them -- **OpenRouter API** (`openrouter`) — Multi-model AI gateway (chat, embeddings, audio transcription/speech, video) -- **Web3 infrastructure** — BlockPI RPC (`rpc`, 13 actions) and Binance Web3 API (`binance-web3-api`, 58 actions); read `guides/blockpi.md` or `guides/binance_web3.md` before calling them +- **OpenRouter API** (`openrouter`) — Multi-model AI gateway (chat, embeddings, Decisions, audio transcription/speech, video) +- **Web3 infrastructure** — BlockPI EVM (`rpc`), dedicated non-EVM RPC and Aptos REST/BCS services, and Binance Web3 API (`binance-web3-api`); read `guides/blockpi.md` or `guides/binance_web3.md` before calling them The full catalog also spans many other categories — crypto/on-chain data, CEX market data, stocks & macro, social platforms, news, weather, and more. Discover them with `search` / `services`. @@ -329,9 +347,9 @@ When the user's task involves these workflows, read the corresponding guide file - **`guides/google_search.md`** — Google Search: web, realtime, news, image, video, scholar, maps, places, shopping - **`guides/serper.md`** — direct Serper v7 API: 12 provider-native actions, object-or-array mini-batches, Reviews pagination and batch exception, Lens, dynamic per-credit billing, and the current Webpage service boundary - **`guides/crypto.md`** — Crypto (加密货币): on-chain token price/overview/holders/security/OHLCV, wallet analytics, DEX pairs, CEX spot prices by symbol, news — covers contract-address vs symbol addressing and multi-chain -- **`guides/domains.md`**, **`guides/blockpi.md`**, **`guides/binance_web3.md`** — unverified domain search, batch availability checks, domain purchase and DNS writes; BlockPI EVM JSON-RPC; and the official Binance Web3 API catalog. Read the domain guide before using domain capabilities; read the other matching guide before any mutation, transaction build, signing, or broadcast +- **`guides/domains.md`**, **`guides/blockpi.md`**, **`guides/binance_web3.md`** — unverified domain search, batch availability checks, domain purchase and DNS writes; BlockPI EVM/non-EVM RPC and Aptos JSON/BCS; and the official Binance Web3 API catalog. Read the domain guide before using domain capabilities; read the other matching guide before any mutation, transaction build, signing, or broadcast - **`guides/ai.md`** — AI (人工智能): synchronous or SSE-streamed text, embeddings, asynchronous image/video generation with `task wait`, text-to-speech, and speech-to-text -- **`guides/ai_gateway.md`** — xAPI AI Gateway: Claude Code and Anthropic/OpenAI SDK setup, model discovery, routing strategies, streaming, fallback, routing/billing headers, direct media endpoints, and known limitations +- **`guides/ai_gateway.md`** — xAPI AI Gateway: Claude Code and Anthropic/OpenAI SDK setup, typed Decisions, model discovery, routing strategies, streaming, fallback, routing/billing headers, direct media endpoints, and known limitations - **`guides/ws_gateway.md`** — xAPI WebSocket Gateway: GPT Live, OpenAI Realtime, streaming ASR/TTS, simultaneous interpretation, podcast generation, service/path routing, browser authentication, native protocols, limits, billing, close codes, and reconnects - **`guides/sandbox.md`** — managed Sandbox compute: AI tool selection, one-shot and multi-step lifecycles, provider pinning, files, Cloudflare Web previews, suspension, GPU jobs, parallel agents, cleanup recovery, audit/history, and billing verification - **`guides/workers.md`** — xAPI-hosted Cloudflare Workers: new-project and Wrangler import flows, plan/push/promote/rollback, no-Git and CI operation, Worker vs Sandbox selection, managed KV/D1/R2/DO/Queue/Workflow and Secrets, persistent schedules, managed DNS/TLS, Tail observability, usage settlement and hard budget/balance guards, Artifact deployment, API-key instance visibility, audit, and deletion diff --git a/skills/xapi/guides/ai.md b/skills/xapi/guides/ai.md index 48515a8..40e87a2 100644 --- a/skills/xapi/guides/ai.md +++ b/skills/xapi/guides/ai.md @@ -2,7 +2,7 @@ Complete guide for AI capabilities via xAPI — text chat, summarize/rewrite, embeddings, asynchronous image/video generation, text-to-speech, and speech-to-text. -All AI endpoints are **built-in capabilities** (`--source capability`). Pass parameters with `--input` as a JSON object. +The `ai.*` calls below are **built-in capabilities** (`--source capability`). Pass parameters with `--input` as a JSON object. Decisions uses a separate native HTTP Gateway protocol; see [Decisions](ai_gateway.md#decisions) for model discovery, typed questions/answers, costs and variant handling. Do not send Decisions through `ai.text.chat.auto` or invent an `ai.decisions.*` capability. This guide covers CLI capabilities. For Claude Code, Anthropic/OpenAI SDKs, streaming HTTP APIs, and Gateway routing, read `guides/ai_gateway.md`. For full-duplex OpenAI Realtime or provider-native streaming ASR/TTS, read `guides/ws_gateway.md`. diff --git a/skills/xapi/guides/ai_gateway.md b/skills/xapi/guides/ai_gateway.md index d1ee71c..09b3a0b 100644 --- a/skills/xapi/guides/ai_gateway.md +++ b/skills/xapi/guides/ai_gateway.md @@ -1,6 +1,6 @@ # AI Gateway Guide -Use xAPI's public AI Gateway when a tool or application expects an Anthropic- or OpenAI-compatible HTTP API. For one-off agent calls from the terminal, prefer the `ai.*` CLI capabilities documented in `guides/ai.md`. For full-duplex OpenAI Realtime, streaming ASR/TTS, simultaneous interpretation, or other WebSocket sessions, read `guides/ws_gateway.md`. +Use xAPI's public AI Gateway for Anthropic/OpenAI-compatible HTTP APIs and the separate Decisions protocol. For one-off calls supported by `ai.*` CLI capabilities, read `guides/ai.md`. Decisions uses native HTTP below. For full-duplex OpenAI Realtime, streaming ASR/TTS, simultaneous interpretation, or other WebSocket sessions, read `guides/ws_gateway.md`. ## Contents @@ -10,6 +10,7 @@ Use xAPI's public AI Gateway when a tool or application expects an Anthropic- or - [OpenAI-compatible setup](#openai-compatible-setup) - [Select a model in the URL](#select-a-model-in-the-url) - [Supported endpoints](#supported-endpoints) +- [Decisions](#decisions) - [Routing strategies](#routing-strategies) - [Provider affinity](#provider-affinity) - [Streaming and protocol translation](#streaming-and-protocol-translation) @@ -34,7 +35,7 @@ Use the AI Gateway when: - the caller needs streaming SSE responses; - the caller should use model normalization, protocol translation, provider health checks, and fallback without selecting an upstream manually. -The Gateway is a separate HTTP interface. Do not wrap Gateway request bodies in the xAPI action format (`action_id` / `input`). Send the native Anthropic or OpenAI request body directly. +The Gateway is a separate HTTP interface. Do not wrap Gateway request bodies in the xAPI action format (`action_id` / `input`). Send the native request body directly. Decisions does not use the chat capabilities or translate to Chat Completions. ## Base URLs and authentication @@ -145,9 +146,105 @@ This also works for Anthropic Messages, OpenAI Responses, and embeddings. Use it | Image generation | `POST /v1/images/generations` | No | | Video generation | `POST /v1/videos` | No | | Rerank | `POST /v1/rerank` | No | +| Decisions | `POST /v1/decisions` | No | For strategy-aware endpoints, insert a strategy before `/v1`, for example `/cost/v1/responses`. Without a prefix, the Gateway uses `default`. +## Decisions + +Discover current compatible Canonical models before choosing one: + +```bash +curl 'https://ai.xapi.to/v1/models?interface=decisions&output=decisions' +``` + +This returns `{object:"list",data:[...]}` without pagination. Each entry includes +`id`, `input_modalities`, `output_modalities`, `capabilities` and `provider_count`. +Filters `interface`, `input` and `output` each accept a single value; add +`input=file` only when file input is needed. Empty results are valid; a failed +discovery request is not evidence that no models exist. + +Use the discovered ID in a native JSON request. This example uses `jev-1-14`; +availability and actual cost depend on the current serving provider: + +```bash +curl --include https://ai.xapi.to/v1/decisions \ + -H 'XAPI-Key: ' \ + -H 'Content-Type: application/json' \ + --data '{ + "model":"jev-1-14", + "state":"I need a refund for a failed payment today.", + "questions":{ + "refund":{"type":"noul","instructions":"Is a refund requested?"}, + "team":{ + "type":"choice","instructions":"Choose the support team.", + "criteria":{"billing":"Payments and refunds","technical":"Bugs and outages"} + }, + "urgency":{ + "type":"score","instructions":"Rate urgency.", + "criteria":["No time pressure","Soon","Today"] + } + } + }' +``` + +Example answer shape, with provider extensions and usage omitted for brevity: + +```json +{ + "answers": { + "refund": {"type": "noul", "noul": 0.99}, + "team": {"type": "choice", "choice": "billing"}, + "urgency": {"type": "score", "score": 1.99} + } +} +``` + +- `state` accepts a string, object or array. `questions` is a non-empty named + object with `choice`, `noul` or `score` types. Every requested question needs + a matching answer of the same type. +- Noul is a number from 0 to 1, not a boolean. Choice must belong to explicit + criteria; Score is non-negative and cannot exceed the last criteria index. + Fractional scores are valid. Choice/Score confidence/probabilities, when + present, must be in `[0,1]`; distributions must cover explicit criteria. +- Send `stream` omitted or `false`. There is no SSE, batch request or `:batch` + model support. Do not add a strategy/model segment before `/v1/decisions`. +- Text-only models reject top-level image/audio/video/file state inputs; use a + compatible model for those modalities. Provider-defined state content is + otherwise retained. +- Raw mapped aliases such as `typesafe/jev-1.14` can also be used. Preserve + requested variants: `mercury-decide:free` or raw + `inception/mercury-decide:free` requires a free candidate and cannot fall back + to the paid base. A free candidate may be unavailable or rate limited. +- Invalid upstream answers can trigger another eligible provider; already + incurred costs remain part of the aggregate. A failed request is not always + free. Local schema/model rejection before upstream execution is zero cost. + +### Public catalog and price display + +The marketplace catalog is a different API: + +```bash +curl 'https://api.xapi.to/api/models/catalog?window=30d&limit=200&offset=0' +``` + +Its shape is `{total,limit,offset,window,items}`. `window` accepts `24h`, `7d` +or `30d` (default); limit defaults to 50 and is capped at 200. Read all pages, +advancing offset by the number of returned items until it reaches `total`. +An empty page before total is incomplete data. This endpoint has no +`interface/output` filter: filter the collected items by +`modality.output.includes("decisions")`. Do not stop after the first 200 models +or hard-code a model count. + +Catalog `bestInputPer1M`, `bestOutputPer1M`, provider rates and eligible endpoint +`modelQuotes` are display prices in USD per million tokens. `pricingMode` can +be `fixed`, `actual_cost` or `unknown`; dynamic quotes are estimates, not final +charges. `null`/missing means unknown and should display `—`; `0` is an explicit +zero rate. Canonical OpenRouter reference prices are comparison data. +Complex cost rules, including caps/clamps, can leave provider prices unknown +even when official reference prices exist. Do not remove billing limits or +infer a free quote from a paid base rate. See [billing headers](#routing-and-billing-headers). + ## Routing strategies Accepted strategies: @@ -192,7 +289,14 @@ Successful routed responses can include: | `X-XAPI-Cost` | Aggregated USD cost for the request attempts. | | `X-XAPI-Cost-Unit` | Cost currency, currently `USD`. | | `X-XAPI-Billing` | Billing status. | -| `X-XAPI-Billing-Type` | Billing mode, such as `PER_TOKEN` or `ASYNC`. | +| `X-XAPI-Billing-Type` | Billing mode, such as `PER_TOKEN`, `DYNAMIC` or `ASYNC`. | +| `X-XAPI-Cost-Estimated` / `X-XAPI-Cost-Reserved` | Estimate and hold amount when provided; not actual consumption. | + +`X-AI-Cost` is a compatibility cost header. Use `X-XAPI-Cost` for the aggregate +across attempts; the final response's upstream `usage.cost` alone excludes any +earlier charged attempts. Gateway responses currently do not expose a reliable +xAPI receipt request ID. A provider response `id` is not a receipt ID, and an +individual attempt receipt must not be described as the aggregate receipt. Do not use provider names as a stable application contract. Provider selection can change with availability, configuration, pricing, and routing policy. @@ -257,7 +361,7 @@ Rerank currently routes directly through OpenRouter rather than the strategy-bas - **Model not found** → use `GET /v1/models` and select a currently routable model ID. - **No candidates / 503** → no active provider currently serves that model and protocol combination; retry later or choose another model. - **Unsupported modality** → choose a model that accepts the request's text, image, or other input type. -- **All providers failed** → retry with backoff; changing strategy may reorder candidates but cannot make an unavailable model routable. +- **All providers failed** → inspect the error and any reported aggregate cost before deciding on a retry; changing strategy cannot make an unavailable model routable. Do not blindly repeat charged or ambiguous requests. - **Async task failed or expired** → inspect the poll result and resubmit when appropriate. Never send the xAPI key to a domain outside `*.xapi.to`. Do not print, persist, or share the key, payment URLs, or authenticated polling URLs. diff --git a/skills/xapi/guides/blockpi.md b/skills/xapi/guides/blockpi.md index fb57e61..494b5d9 100644 --- a/skills/xapi/guides/blockpi.md +++ b/skills/xapi/guides/blockpi.md @@ -12,8 +12,134 @@ npx xapi-to get rpc.network npx xapi-to search "BlockPI RPC" --source api --page-size 100 ``` -The catalog currently lists each action at `$0.000003/call`; treat `get` as the -authority because pricing and supported networks may change. +Use the live `get` response as the authority for price, schema, methods and +availability. Prices and supported networks can change; do not apply an EVM +action's price to a separate chain service. + +## Select a non-EVM service + +These are separate mainnet services, not values of `rpc.network`: + +| Chain | Action lookup | Public Gateway route | Request format | +|---|---|---|---| +| Solana | `solana` | `POST https://solana.p.xapi.to/` | Single JSON-RPC 2.0 object; positional `params` | +| NEAR | `near` | `POST https://near.p.xapi.to/` | Single JSON-RPC 2.0 object; method-specific named or positional `params` | +| Starknet | `starknet` | `POST https://starknet.p.xapi.to/` | Single JSON-RPC 2.0 object; method-specific named or positional `params` | +| Bitcoin | `bitcoin`, `bitcoin.block`, `bitcoin.broadcast` | `POST /`, `/block`, `/broadcast` on `https://bitcoin.p.xapi.to` | Single JSON-RPC 1.0 or 2.0 object; positional `params` | +| Aptos | `aptos.v1`, `aptos.v1_transactions`, `aptos.v1_transactions_simulate`, `aptos.v1_transactions_batch` | `/v1` and transaction routes on `https://aptos.p.xapi.to` | REST; JSON or signed-transaction BCS as specified below | + +Discover before calling, then inspect the exact returned schema: + +```bash +npx xapi-to search "Solana Mainnet" --source api --page-size 100 +npx xapi-to get solana +npx xapi-to get near +npx xapi-to get starknet +npx xapi-to get bitcoin +npx xapi-to get aptos.v1_transactions +``` + +A bare service ID such as `solana` represents `/`; `solana.root` represents a +literal `/root` path and is not a root alias. A human ID can have multiple HTTP +methods: choose the matching schema and pass the HTTP method explicitly. Prefer +the exact endpoint `uuid` returned by discovery when selecting one endpoint; +never invent a UUID or replace a missing/stale UUID with a human alias. Re-discover +instead. For example, this read uses the root Solana Action: + +```bash +npx xapi-to call solana --input '{ + "method":"POST", + "body":{"jsonrpc":"2.0","id":1,"method":"getSlot","params":[]} +}' +``` + +These JSON-RPC services admit one request object per HTTP call, not batch arrays +or notifications. Inspect JSON-RPC `error` even with HTTP 200. Bitcoin wallet +methods and public `/mempool` access are unavailable in the maintained contract. +NEAR and Starknet submission methods are disabled in the maintained contract; +do not infer write support from their read operations. Solana, Bitcoin and Aptos +submission also depend on operator gates and the serving revision. An advertised +route is not proof that its gate is enabled. HTTPS/HTTP/2 transport support does +not imply gRPC or WebSocket subscriptions. + +## Aptos JSON and BCS + +Inspect the serving contract and exact endpoint before preparing a request: + +| Method and path | Request media type | Meaning | +|---|---|---| +| `GET /v1` | No body | Ledger information | +| `POST /v1/transactions` | `application/json` or `application/x.aptos.signed_transaction+bcs` | One caller-signed transaction | +| `POST /v1/transactions/simulate` | Same JSON or signed-transaction BCS types | Simulation; inspect each VM result | +| `POST /v1/transactions/batch` | `application/x.aptos.signed_transaction+bcs` only | BCS vector of 1–20 signed transactions | + +All three transaction POST routes have a 256 KiB body limit. Generic CLI/MCP +Action execution currently encodes request bodies as JSON; it does not upload +raw BCS input. Use the public Gateway with `--data-binary @file` for BCS. CLI +`--output` saves response bytes and does not turn JSON input into a BCS upload. +Do not send a filename, base64/hex text, or a JSON array as the raw BCS body. + +For JSON simulation, prepare `simulation.json` using the intended transaction +fields, current sender sequence number and corresponding public key, with an +SDK simulation authenticator or deliberately non-verifying signature. Ed25519 +simulation commonly uses 64 zero-filled signature bytes; valid signatures are +rejected by the simulation API. Simulation does not broadcast, but can still +incur a billed call. Use a normally configured xAPI key in `XAPI_KEY`: + +```bash +curl -sS -i https://aptos.p.xapi.to/v1/transactions/simulate \ + -H "XAPI-KEY: $XAPI_KEY" \ + -H 'Content-Type: application/json' \ + --data-binary @simulation.json +``` + +The same simulation can be encoded by an Aptos SDK as one complete +`SignedTransaction` with a non-verifying authenticator: + +```bash +curl -sS -i https://aptos.p.xapi.to/v1/transactions/simulate \ + -H "XAPI-KEY: $XAPI_KEY" \ + -H 'Content-Type: application/x.aptos.signed_transaction+bcs' \ + --data-binary @simulation.bcs +``` + +Single submission uses `/v1/transactions` with the JSON signed transaction or +raw SDK-encoded `SignedTransaction` file and the matching media type. Submission +examples require the explicit transaction approval described under Transaction +safety; a simulation result alone does not authorize a broadcast. + +For an approved batch, serialize a BCS **vector**, including its ULEB128 length +prefix (counts 1–20 use one byte), followed by the complete signed transactions. +Use the SDK vector serializer; concatenating single-transaction files omits the +vector length and is invalid. After transaction approval: + +```bash +curl -sS -i https://aptos.p.xapi.to/v1/transactions/batch \ + -H "XAPI-KEY: $XAPI_KEY" \ + -H 'Content-Type: application/x.aptos.signed_transaction+bcs' \ + --data-binary @approved-signed-transactions.bcs +``` + +JSON batch requests return HTTP 415. HTTP 206 means partial batch acceptance: +inspect per-transaction failures and query the accepted transactions before any +retry. Neither 202/single acceptance nor 206/batch acceptance proves inclusion, +execution success or finality; confirm each hash on-chain. HTTP 200 simulation +can contain `success:false` and a VM error, so it is not a successful transfer. + +Production forwarding verification on 2026-10-16 used only invalid signatures: +single JSON/BCS returned signature rejection; BCS batch reached upstream batch +validation; simulation returned HTTP 200 with a failed VM result. Plain, gzip +and chunked BCS variants produced consistent logical payload and validation +results. This verifies forwarding and rejection, not valid transaction acceptance +or on-chain confirmation. Use a deliberately invalid, format-complete fixture +for a forwarding check; do not construct a valid signed transaction to obtain 2xx. + +The maintained public Aptos contract has 27 operations. Raw table +`POST /v1/tables/{handle}/raw_item` is hidden pending upstream compatibility. +`hidden` controls public discovery/export and does not disable an existing +runtime route. Do not infer availability from its omission or advertise it as +supported. Its binary response type `application/x-bcs` is different from the +signed-transaction **request** media type above. ## Generic JSON-RPC @@ -104,9 +230,12 @@ npx xapi-to call rpc.chain_getBalance --input '{ ## Transaction safety Reads such as block, balance, code, logs, and receipt queries are non-mutating. -Methods such as `eth_sendRawTransaction` can create irreversible on-chain +Methods such as `eth_sendRawTransaction`, Solana `sendTransaction`, Bitcoin +`sendrawtransaction` and Aptos single/batch submission can create irreversible on-chain effects even though they use the same generic action. Before submitting a signed transaction, obtain explicit approval for the chain, sender, recipient, value, calldata, gas policy, and transaction hash workflow. Do not send private keys or seed phrases through xAPI, and do not automatically retry an ambiguous -broadcast. Check the transaction hash or sender nonce first. +broadcast. Check the transaction hash or sender nonce first. For a batch, approval +must cover every transaction; partial acceptance does not authorize resending +the complete batch. diff --git a/skills/xapi/guides/provider.md b/skills/xapi/guides/provider.md index e5cc5ac..27f7d40 100644 --- a/skills/xapi/guides/provider.md +++ b/skills/xapi/guides/provider.md @@ -23,7 +23,9 @@ Use narrowly scoped keys. Common scopes are: - `service:delete` only for deliberate removal workflows Scope permission and ownership are independent. A key with a scope still cannot -manage another provider's service or Skill. +manage another provider's service or Skill. These commands validate the owner +path; they do not prove a separate administrator can view or review a working +revision. ## Create and describe a service @@ -119,6 +121,23 @@ if they were evidence. automatically retry this write after an ambiguous transport failure. Read the version overview and review state before deciding whether to submit again. +A passing review can automatically publish and switch the serving revision. +In `provider versions`, use `majors[].working.id` for draft edits and verify +`servingRevisionId` after publication. Cloning creates new endpoint UUIDs while +preserving `endpointKey`; get the working contract and use its endpoint IDs for +patches. See the [provider lifecycle reference](../../xapi-provider/reference/lifecycle-management.md) +for PATCH/PUT matching and removal rules. + +Use the overview's major/revision label to select that contract, for example +`npx xapi-to provider get --version v1.4`; confirm its revision ID +matches `working.id`. A working revision already `IN_REVIEW` is not editable. + +For Decisions model configuration and DYNAMIC billing, read +[AI provider contracts](../../xapi-provider/reference/ai-spec-and-models.md). +For an HTTP/2 upstream, read [HTTP transport](../../xapi-provider/reference/http-transport.md) +before updating `openApiSpec`; neither workflow requires a new CLI transport +flag. + ## Create, upload, and link the usage Skill Generate a service-specific starting point from the currently serving endpoints: @@ -129,7 +148,10 @@ npx xapi-to provider skill scaffold \ ``` The scaffold command refuses to overwrite an existing file unless `--force` is -explicitly supplied. Complete the instructions and metadata, then submit either +explicitly supplied. Check generated examples against the serving contract: +strip internal named-base selectors such as `{{alpha}}` from public paths, +use the real HTTP method, body and media type, and use raw Gateway requests for +BCS input. Complete the instructions and metadata, then submit either a local directory or a public GitHub tree: ```bash @@ -171,6 +193,10 @@ to remove the primary association. The context response reports drift when host, major version, or serving endpoints no longer match the stored fingerprint; update and resubmit the Skill rather than merely overwriting the fingerprint. +The current fingerprint covers host, major and endpoint keys. `drift: false` +does not certify unchanged schemas, MIME types or transport; review those when +the contract changes even if endpoint keys remain identical. + ## Observe and recover ```bash @@ -183,9 +209,13 @@ npx xapi-to provider events --after '' --limit 50 Pass event cursors back unchanged. Metrics and events are owner-scoped; usage events are also restricted to the current key where applicable. -Verify the finalized cost of a canary or provider call with its receipt ID. Use -the `X-XAPI-Request-Id` response header or the final `xapi.usage` SSE event, and -wait when asynchronous billing has not finalized yet: +Verify the finalized cost of a canary or provider call with an xAPI receipt ID +when the selected interface exposes it, such as the direct service Gateway's +`X-XAPI-Request-Id` header or a documented `xapi.usage` SSE event. Ordinary CLI +JSON calls currently return the body without exposing response headers, and +the AI Gateway does not yet provide a reliable receipt-ID response contract. +Do not substitute an upstream response `id`. With a known xAPI request ID, wait +when asynchronous billing has not finalized yet: ```bash npx xapi-to usage diff --git a/src/commands/provider-onboarding.ts b/src/commands/provider-onboarding.ts index def0769..f5225b0 100644 --- a/src/commands/provider-onboarding.ts +++ b/src/commands/provider-onboarding.ts @@ -13,9 +13,9 @@ COMMANDS --private-headers-file Upstream credentials as a JSON object update --revision --file config.json --mode merge|replace PATCH merge (default) or PUT replacement - --allow-new-endpoints Allow merge entries without IDs to create endpoints + --allow-new-endpoints Allow ID-less merge entries; backend key matching applies submit --revision - --changelog Submit for review; this alone is not publication + --changelog Submit for review; passing review may publish review --revision Read latest review and previous attempts wait --revision diff --git a/src/tests/skill-live-services-guide.test.ts b/src/tests/skill-live-services-guide.test.ts index 044e75e..5f63f84 100644 --- a/src/tests/skill-live-services-guide.test.ts +++ b/src/tests/skill-live-services-guide.test.ts @@ -127,7 +127,7 @@ describe('bundled xAPI live-service guides', () => { 'never ask for, expose, or forward', 'eth_sendRawTransaction', 'keys or seed phrases', 'fails closed', 'public RPC endpoints', ]) expect(blockpi).toContain(required); - expectValidJsonExamples(blockpi, 3); + expectValidJsonExamples(blockpi, 4); expectBalancedFences(blockpi); });