From 1836b628e3a3702bbf46de096e2ba26f83456e07 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 21:55:17 +0200 Subject: [PATCH 01/13] docs(providers): call the AI Gateway from the OpenAI and Anthropic clients Document the vendor-neutral /ai/v1 endpoint and the Anthropic Messages endpoint for code outside Veryfront, with a project key that has Write. The guide tests pin each documented base URL to the one the SDK routes Veryfront Cloud models to, including a vendor the SDK has no entry for. Refs veryfront/veryfront-issue-inbox#1573 --- docs/guides/providers.md | 68 ++++++++++++++++++++++++++ tests/docs/guide-code-examples.test.ts | 39 +++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/docs/guides/providers.md b/docs/guides/providers.md index fd8db8ea36..ab466ef6a1 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -61,6 +61,74 @@ export default agent({ `VERYFRONT_DEFAULT_MODEL` can select another gateway default for omitted models and `model: "auto"`. It is optional. +### Call the AI Gateway from other clients + +Code outside Veryfront can use the AI Gateway through the official OpenAI and +Anthropic clients. Create a project API key with the Write permission in Studio +under **Settings > API Keys**. Inference requests are writes, so a Read-only key +is refused. The key starts with `vf_` and is bound to its project. + +Name the model as `/`. List the models your key can use: + +```bash +curl https://api.veryfront.com/ai/v1/models \ + -H "Authorization: Bearer $VERYFRONT_API_KEY" +``` + +Every listed model works on the OpenAI protocol at +`https://api.veryfront.com/ai/v1`, including vendors this SDK has no built-in +entry for: + +```ts +import OpenAI from "openai"; + +const client = new OpenAI({ + baseURL: "https://api.veryfront.com/ai/v1", + apiKey: process.env.VERYFRONT_API_KEY, +}); + +const completion = await client.chat.completions.create({ + model: "mistral/mistral-small-2503", + messages: [{ role: "user", content: "Say hello." }], +}); +``` + +```bash +curl https://api.veryfront.com/ai/v1/chat/completions \ + -H "Authorization: Bearer $VERYFRONT_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"model":"mistral/mistral-small-2503","messages":[{"role":"user","content":"Say hello."}]}' +``` + +Anthropic models also speak the Anthropic Messages protocol. The Anthropic +client adds `/v1/messages` to its base URL: + +```ts +import Anthropic from "@anthropic-ai/sdk"; + +const client = new Anthropic({ + baseURL: "https://api.veryfront.com/ai/anthropic", + apiKey: process.env.VERYFRONT_API_KEY, +}); + +const message = await client.messages.create({ + model: "anthropic/claude-sonnet-4-6", + max_tokens: 1024, + messages: [{ role: "user", content: "Say hello." }], +}); +``` + +```bash +curl https://api.veryfront.com/ai/anthropic/v1/messages \ + -H "x-api-key: $VERYFRONT_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "Content-Type: application/json" \ + -d '{"model":"anthropic/claude-sonnet-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Say hello."}]}' +``` + +Inside a Veryfront agent, use a `veryfront-cloud//` string +instead. The SDK picks the same endpoints for you. + ## Runtime conventions (recommended) For most projects, omit `model` to use the default for your inference path. Set diff --git a/tests/docs/guide-code-examples.test.ts b/tests/docs/guide-code-examples.test.ts index 9e29c0f53e..3c02460755 100644 --- a/tests/docs/guide-code-examples.test.ts +++ b/tests/docs/guide-code-examples.test.ts @@ -77,6 +77,7 @@ import { parsePushArgs } from "../../cli/commands/push/command.ts"; import { parseCliArgs } from "../../cli/shared/args.ts"; import { AUTH_PRESETS } from "../../cli/scaffold/engine.ts"; import { getTemplate } from "../../templates/index.ts"; +import { getVeryfrontCloudGatewayBaseUrl } from "../../src/provider/veryfront-cloud/shared.ts"; const EXISTING_GUIDE_EXAMPLE_SUITE = [ "agents.md", @@ -204,6 +205,44 @@ describe("Guide code example coverage", () => { }); }); +describe("Guide: providers.md", () => { + const api = "https://api.veryfront.com"; + + async function gatewayClientSection(): Promise { + const guide = await readGuide("providers.md"); + const start = guide.indexOf("### Call the AI Gateway from other clients"); + assert(start !== -1, "providers.md documents calling the AI Gateway from other clients"); + const end = guide.indexOf("\n## ", start); + return guide.slice(start, end === -1 ? undefined : end); + } + + it("gives the OpenAI client the base URL the SDK sends OpenAI-protocol models to", async () => { + const section = await gatewayClientSection(); + const base = getVeryfrontCloudGatewayBaseUrl(api, "openai"); + + assertStringIncludes(section, `baseURL: "${base}"`); + assertStringIncludes(section, `curl ${base}/chat/completions`); + }); + + it("gives the Anthropic client the base URL the SDK sends Anthropic-protocol models to", async () => { + const section = await gatewayClientSection(); + const base = getVeryfrontCloudGatewayBaseUrl(api, "anthropic"); + + // The Anthropic client appends /v1/messages to its base URL. + assertEquals(base.endsWith("/v1"), true); + assertStringIncludes(section, `baseURL: "${base.slice(0, -"/v1".length)}"`); + assertStringIncludes(section, `curl ${base}/messages`); + }); + + it("sends a vendor the SDK does not know over the OpenAI protocol", async () => { + const section = await gatewayClientSection(); + + assertEquals(getVeryfrontCloudGatewayBaseUrl(api, "acme-labs"), `${api}/ai/v1`); + assertStringIncludes(section, "`/`"); + assertStringIncludes(section, `curl ${api}/ai/v1/models`); + }); +}); + describe("Guide: middleware.md", () => { it("uses the global CORS policy for preflight and the actual response", async () => { const config = defineConfig({ From 01c4168f5c18f60e7cf4a607d8dbde00cd7e7596 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 22:25:32 +0200 Subject: [PATCH 02/13] docs(providers): keep the guide test off the host route override Pin the neutral routes in the guide test, so VERYFRONT_CLOUD_GATEWAY_ROUTES=vendor on the host does not fail it, and say how the example key variable relates to VERYFRONT_API_TOKEN. --- docs/guides/providers.md | 2 ++ tests/docs/guide-code-examples.test.ts | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/guides/providers.md b/docs/guides/providers.md index ab466ef6a1..5300f85f98 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -67,6 +67,8 @@ Code outside Veryfront can use the AI Gateway through the official OpenAI and Anthropic clients. Create a project API key with the Write permission in Studio under **Settings > API Keys**. Inference requests are writes, so a Read-only key is refused. The key starts with `vf_` and is bound to its project. +The examples read it from `VERYFRONT_API_KEY`, a name for your own code only. +The Veryfront CLI and SDK read `VERYFRONT_API_TOKEN`, described above. Name the model as `/`. List the models your key can use: diff --git a/tests/docs/guide-code-examples.test.ts b/tests/docs/guide-code-examples.test.ts index 3c02460755..dfd843f37a 100644 --- a/tests/docs/guide-code-examples.test.ts +++ b/tests/docs/guide-code-examples.test.ts @@ -216,9 +216,18 @@ describe("Guide: providers.md", () => { return guide.slice(start, end === -1 ? undefined : end); } + // The guide documents the default routes, so a host that opts back into + // vendor routes must not change what this suite compares against. + function neutralBaseUrl(provider: string): Promise { + return withEnv( + { VERYFRONT_CLOUD_GATEWAY_ROUTES: "" }, + () => Promise.resolve(getVeryfrontCloudGatewayBaseUrl(api, provider)), + ); + } + it("gives the OpenAI client the base URL the SDK sends OpenAI-protocol models to", async () => { const section = await gatewayClientSection(); - const base = getVeryfrontCloudGatewayBaseUrl(api, "openai"); + const base = await neutralBaseUrl("openai"); assertStringIncludes(section, `baseURL: "${base}"`); assertStringIncludes(section, `curl ${base}/chat/completions`); @@ -226,7 +235,7 @@ describe("Guide: providers.md", () => { it("gives the Anthropic client the base URL the SDK sends Anthropic-protocol models to", async () => { const section = await gatewayClientSection(); - const base = getVeryfrontCloudGatewayBaseUrl(api, "anthropic"); + const base = await neutralBaseUrl("anthropic"); // The Anthropic client appends /v1/messages to its base URL. assertEquals(base.endsWith("/v1"), true); @@ -237,7 +246,7 @@ describe("Guide: providers.md", () => { it("sends a vendor the SDK does not know over the OpenAI protocol", async () => { const section = await gatewayClientSection(); - assertEquals(getVeryfrontCloudGatewayBaseUrl(api, "acme-labs"), `${api}/ai/v1`); + assertEquals(await neutralBaseUrl("acme-labs"), `${api}/ai/v1`); assertStringIncludes(section, "`/`"); assertStringIncludes(section, `curl ${api}/ai/v1/models`); }); From 37e6c40723d215ee5f6f17628061de0f0574c686 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 23:27:09 +0200 Subject: [PATCH 03/13] test(docs): execute provider examples with official clients --- deno.json | 2 +- deno.lock | 51 +++++++++++++++ docs/guides/providers.md | 7 +- .../docs/provider-client-examples.test.ts | 65 +++++++++++++++++++ 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 tests/integration/docs/provider-client-examples.test.ts diff --git a/deno.json b/deno.json index 9ffb352497..43c6be928d 100644 --- a/deno.json +++ b/deno.json @@ -558,7 +558,7 @@ "docs:public:check": "deno run --config=scripts/test.deno.json --allow-read scripts/docs/validate-public-docs.ts", "docs:coverage": "deno run --allow-read scripts/docs/docs-coverage.ts", "docs:copy": "rm -rf ../../docs/docs/code/api-reference && cp -r docs/api-reference/ ../../docs/docs/code/api-reference/", - "docs:validate": "deno run --allow-read --allow-run=git scripts/docs/validate-tracked-docs.ts && deno run --allow-read scripts/docs/validate-api-reference.ts && deno run --allow-read scripts/docs/validate-guides.ts && deno run --config=scripts/test.deno.json --allow-read scripts/docs/validate-public-docs.ts && deno test --config=scripts/test.deno.json --no-check --allow-read scripts/docs/docs-coverage.test.ts && DENO_TESTING=1 deno test --no-check --allow-read --allow-env=DENO_TESTING tests/docs/guide-contracts.test.ts tests/docs/guide-content.test.ts && DENO_TESTING=1 deno test --no-check --allow-all tests/docs/guide-examples.test.ts tests/docs/guide-code-examples.test.ts && deno run -A scripts/lint/check-doc-links.ts", + "docs:validate": "deno run --allow-read --allow-run=git scripts/docs/validate-tracked-docs.ts && deno run --allow-read scripts/docs/validate-api-reference.ts && deno run --allow-read scripts/docs/validate-guides.ts && deno run --config=scripts/test.deno.json --allow-read scripts/docs/validate-public-docs.ts && deno test --config=scripts/test.deno.json --no-check --allow-read scripts/docs/docs-coverage.test.ts && deno task test:file tests/integration/docs/provider-client-examples.test.ts && DENO_TESTING=1 deno test --no-check --allow-read --allow-env=DENO_TESTING tests/docs/guide-contracts.test.ts tests/docs/guide-content.test.ts && DENO_TESTING=1 deno test --no-check --allow-all tests/docs/guide-examples.test.ts tests/docs/guide-code-examples.test.ts && deno run -A scripts/lint/check-doc-links.ts", "docs:verify-npm": "node scripts/docs/verify-npm-exports.mjs && node scripts/docs/verify-npm-node.mjs", "docs:check-links": "deno run -A scripts/lint/check-doc-links.ts", "lint:ban-zod": "deno run --allow-read scripts/lint/ban-zod-imports.ts", diff --git a/deno.lock b/deno.lock index ceaf817840..8efb5e65ec 100644 --- a/deno.lock +++ b/deno.lock @@ -19,6 +19,7 @@ "jsr:@std/testing@1.0.17": "1.0.17", "jsr:@std/yaml@*": "1.1.0", "jsr:@std/yaml@1.1.0": "1.1.0", + "npm:@anthropic-ai/sdk@0.128.0": "0.128.0_zod@4.3.6", "npm:@aws-sdk/client-s3@3.980.0": "3.980.0", "npm:@aws-sdk/lib-storage@3.980.0": "3.980.0_@aws-sdk+client-s3@3.980.0", "npm:@babel/generator@7.29.1": "7.29.1", @@ -90,6 +91,7 @@ "npm:micromark-util-combine-extensions@2.0.1": "2.0.1", "npm:micromark-util-types@2.0.2": "2.0.2", "npm:micromark@4.0.2": "4.0.2", + "npm:openai@7.23.0": "7.23.0_ws@8.21.1_zod@4.3.6", "npm:parse5@7.3.0": "7.3.0", "npm:pdf-lib@1.17.1": "1.17.1", "npm:playwright@*": "1.60.0", @@ -235,6 +237,18 @@ "json-schema" ] }, + "@anthropic-ai/sdk@0.128.0_zod@4.3.6": { + "integrity": "sha512-tl5cBFZC1jVFrTuQyvQObA4WmuNgcYVeXfriqOumgtLAHUm4gPUj18YnszBW60v0PrjDB8nBDOC6wd9CmHQFlA==", + "dependencies": [ + "json-schema-to-ts", + "standardwebhooks", + "zod@4.3.6" + ], + "optionalPeers": [ + "zod@4.3.6" + ], + "bin": true + }, "@apm-js-collab/code-transformer-bundler-plugins@0.7.1": { "integrity": "sha512-Yidf5GOl60db80UxUtNdKK3pnY7obU/gs0xOfA0SCdnvVLMCvfYIer/egC3TqpPiT0Jg22eg3RlzcO+zKfPMcA==", "dependencies": [ @@ -742,6 +756,9 @@ ], "bin": true }, + "@babel/runtime@7.29.7": { + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==" + }, "@babel/template@7.29.7": { "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dependencies": [ @@ -2694,6 +2711,9 @@ "tslib@2.8.1" ] }, + "@stablelib/base64@1.0.1": { + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, "@standard-schema/spec@1.1.0": { "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" }, @@ -3352,6 +3372,9 @@ "micromatch" ] }, + "fast-sha256@1.3.0": { + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, "fast-uri@3.1.7": { "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==" }, @@ -3797,6 +3820,13 @@ "bignumber.js" ] }, + "json-schema-to-ts@3.1.1": { + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dependencies": [ + "@babel/runtime", + "ts-algebra" + ] + }, "json-schema-traverse@1.0.0": { "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, @@ -4613,6 +4643,17 @@ "protobufjs" ] }, + "openai@7.23.0_ws@8.21.1_zod@4.3.6": { + "integrity": "sha512-0ecOXnFSMNWZq6cUBcTV6Lf93y+fm8BH/+QzFvpoP1UGNOE5pnytRa6HAPGCw+3IS9SIAW7rc1M8yyzN1PiBAQ==", + "dependencies": [ + "ws", + "zod@4.3.6" + ], + "optionalPeers": [ + "ws", + "zod@4.3.6" + ] + }, "pako@1.0.11": { "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, @@ -5242,6 +5283,13 @@ "sql.js@1.14.1": { "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==" }, + "standardwebhooks@1.1.1": { + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "dependencies": [ + "@stablelib/base64", + "fast-sha256" + ] + }, "stream-browserify@3.0.0": { "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", "dependencies": [ @@ -5393,6 +5441,9 @@ "trough@2.2.0": { "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==" }, + "ts-algebra@2.0.0": { + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + }, "tslib@1.14.1": { "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, diff --git a/docs/guides/providers.md b/docs/guides/providers.md index 5300f85f98..bf7e216d82 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -77,9 +77,10 @@ curl https://api.veryfront.com/ai/v1/models \ -H "Authorization: Bearer $VERYFRONT_API_KEY" ``` -Every listed model works on the OpenAI protocol at -`https://api.veryfront.com/ai/v1`, including vendors this SDK has no built-in -entry for: +Use a model that supports Chat Completions with the OpenAI client at +`https://api.veryfront.com/ai/v1`. Models supporting other operations use their +corresponding endpoints. Model identifiers can include vendors this SDK has no +built-in entry for: ```ts import OpenAI from "openai"; diff --git a/tests/integration/docs/provider-client-examples.test.ts b/tests/integration/docs/provider-client-examples.test.ts new file mode 100644 index 0000000000..8a74a4d85c --- /dev/null +++ b/tests/integration/docs/provider-client-examples.test.ts @@ -0,0 +1,65 @@ +import { assertEquals } from "#veryfront/testing/assert.ts"; +import OpenAI from "npm:openai@7.23.0"; +import Anthropic from "npm:@anthropic-ai/sdk@0.128.0"; + +// Run the guide's actual JavaScript with the official clients. Only transport +// and credentials are supplied by the test; the constructor and call stay in +// the extracted example, so client path or authentication changes are visible. +Deno.test("provider guide client snippets send their documented requests", async () => { + const guide = await Deno.readTextFile( + new URL("../../../docs/guides/providers.md", import.meta.url), + ); + const section = guide.split("### Call the AI Gateway from other clients")[1]! + .split("\n## ")[0]!; + const snippets = [...section.matchAll(/```ts\n([\s\S]*?)```/g)].map((match) => match[1]!); + assertEquals(snippets.length, 2); + const requests: Request[] = []; + const clientFetch: typeof fetch = (input, init) => { + requests.push(new Request(input, init)); + return Promise.resolve( + Response.json({ id: "example", content: [], choices: [] }), + ); + }; + const clients = { + OpenAI: class extends OpenAI { + constructor(options: ConstructorParameters[0]) { + super({ ...options, fetch: clientFetch }); + } + }, + Anthropic: class extends Anthropic { + constructor(options: ConstructorParameters[0]) { + super({ ...options, fetch: clientFetch }); + } + }, + }; + const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; + for (const snippet of snippets) { + const executable = snippet.replace(/^import .*;\n/gm, ""); + await new AsyncFunction("OpenAI", "Anthropic", "process", executable)( + clients.OpenAI, + clients.Anthropic, + { env: { VERYFRONT_API_KEY: "example-project-key" } }, + ); + } + assertEquals(requests.length, 2); + assertEquals(requests.map((request) => request.method), ["POST", "POST"]); + assertEquals(requests.map((request) => request.url), [ + "https://api.veryfront.com/ai/v1/chat/completions", + "https://api.veryfront.com/ai/anthropic/v1/messages", + ]); + assertEquals( + requests[0]!.headers.get("authorization"), + "Bearer example-project-key", + ); + assertEquals(requests[1]!.headers.get("x-api-key"), "example-project-key"); + assertEquals(requests[1]!.headers.get("anthropic-version"), "2023-06-01"); + assertEquals(await requests[0]!.json(), { + model: "mistral/mistral-small-2503", + messages: [{ role: "user", content: "Say hello." }], + }); + assertEquals(await requests[1]!.json(), { + model: "anthropic/claude-sonnet-4-6", + max_tokens: 1024, + messages: [{ role: "user", content: "Say hello." }], + }); +}); From e16ad103a65160828a244f66009626da281b6b51 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 23:29:20 +0200 Subject: [PATCH 04/13] docs(providers): use the deployed canonical Messages endpoint --- docs/guides/providers.md | 4 ++-- tests/docs/guide-code-examples.test.ts | 4 ++-- tests/integration/docs/provider-client-examples.test.ts | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/guides/providers.md b/docs/guides/providers.md index bf7e216d82..dab2b72a03 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -110,7 +110,7 @@ client adds `/v1/messages` to its base URL: import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ - baseURL: "https://api.veryfront.com/ai/anthropic", + baseURL: "https://api.veryfront.com/ai", apiKey: process.env.VERYFRONT_API_KEY, }); @@ -122,7 +122,7 @@ const message = await client.messages.create({ ``` ```bash -curl https://api.veryfront.com/ai/anthropic/v1/messages \ +curl https://api.veryfront.com/ai/v1/messages \ -H "x-api-key: $VERYFRONT_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ diff --git a/tests/docs/guide-code-examples.test.ts b/tests/docs/guide-code-examples.test.ts index dfd843f37a..48ead418f5 100644 --- a/tests/docs/guide-code-examples.test.ts +++ b/tests/docs/guide-code-examples.test.ts @@ -233,9 +233,9 @@ describe("Guide: providers.md", () => { assertStringIncludes(section, `curl ${base}/chat/completions`); }); - it("gives the Anthropic client the base URL the SDK sends Anthropic-protocol models to", async () => { + it("gives the Anthropic client the canonical Messages base URL", async () => { const section = await gatewayClientSection(); - const base = await neutralBaseUrl("anthropic"); + const base = `${api}/ai/v1`; // The Anthropic client appends /v1/messages to its base URL. assertEquals(base.endsWith("/v1"), true); diff --git a/tests/integration/docs/provider-client-examples.test.ts b/tests/integration/docs/provider-client-examples.test.ts index 8a74a4d85c..209d6751c2 100644 --- a/tests/integration/docs/provider-client-examples.test.ts +++ b/tests/integration/docs/provider-client-examples.test.ts @@ -13,6 +13,10 @@ Deno.test("provider guide client snippets send their documented requests", async .split("\n## ")[0]!; const snippets = [...section.matchAll(/```ts\n([\s\S]*?)```/g)].map((match) => match[1]!); assertEquals(snippets.length, 2); + assertEquals(snippets.map((snippet) => snippet.split("\n")[0]), [ + 'import OpenAI from "openai";', + 'import Anthropic from "@anthropic-ai/sdk";', + ]); const requests: Request[] = []; const clientFetch: typeof fetch = (input, init) => { requests.push(new Request(input, init)); @@ -45,7 +49,7 @@ Deno.test("provider guide client snippets send their documented requests", async assertEquals(requests.map((request) => request.method), ["POST", "POST"]); assertEquals(requests.map((request) => request.url), [ "https://api.veryfront.com/ai/v1/chat/completions", - "https://api.veryfront.com/ai/anthropic/v1/messages", + "https://api.veryfront.com/ai/v1/messages", ]); assertEquals( requests[0]!.headers.get("authorization"), From c99a9611d972b77248e5c248d1c3e4cd193e8814 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Mon, 28 Sep 2026 00:24:10 +0200 Subject: [PATCH 05/13] fix(build): sync embedded packages for provider client tests --- src/discovery/embedded-npm-packages.generated.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/discovery/embedded-npm-packages.generated.ts b/src/discovery/embedded-npm-packages.generated.ts index 9d71309344..a1cc1e16cc 100644 --- a/src/discovery/embedded-npm-packages.generated.ts +++ b/src/discovery/embedded-npm-packages.generated.ts @@ -18,6 +18,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "@ai-sdk/gateway": ["4.0.31"], "@ai-sdk/provider": ["4.0.4"], "@ai-sdk/provider-utils": ["5.0.14"], + "@anthropic-ai/sdk": ["0.128.0"], "@apm-js-collab/code-transformer": ["0.18.1"], "@apm-js-collab/code-transformer-bundler-plugins": ["0.7.1"], "@apm-js-collab/tracing-hooks": ["0.13.0"], @@ -68,6 +69,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "@babel/helper-string-parser": ["7.29.7"], "@babel/helper-validator-identifier": ["7.29.7"], "@babel/parser": ["7.29.2", "7.29.7"], + "@babel/runtime": ["7.29.7"], "@babel/template": ["7.29.7"], "@babel/traverse": ["7.29.0"], "@babel/types": ["7.29.0", "7.29.7"], @@ -303,6 +305,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "@smithy/util-stream": ["4.7.16"], "@smithy/util-utf8": ["2.3.0", "4.4.16"], "@smithy/util-waiter": ["4.5.16"], + "@stablelib/base64": ["1.0.1"], "@standard-schema/spec": ["1.1.0"], "@swc/wasm": ["1.16.1"], "@tailwindcss/forms": ["0.5.11"], @@ -426,6 +429,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "extend": ["3.0.2"], "fast-deep-equal": ["3.1.3"], "fast-glob": ["3.3.3"], + "fast-sha256": ["1.3.0"], "fast-uri": ["3.1.7"], "fast-xml-builder": ["1.3.0"], "fast-xml-parser": ["5.10.1"], @@ -498,6 +502,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "jsesc": ["3.1.0"], "json-bigint": ["1.0.0"], "json-schema": ["0.4.0"], + "json-schema-to-ts": ["3.1.1"], "json-schema-traverse": ["1.0.0"], "json-stringify-safe": ["5.0.1"], "jszip": ["3.10.1"], @@ -605,6 +610,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "onnxruntime-common": ["1.24.0-dev.20251116-b39e144322", "1.24.3"], "onnxruntime-node": ["1.24.3"], "onnxruntime-web": ["1.26.0-dev.20260416-b7804b056c"], + "openai": ["7.23.0"], "pako": ["1.0.11"], "papaparse": ["5.5.4"], "parse-entities": ["4.0.2"], @@ -683,6 +689,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "space-separated-tokens": ["2.0.2"], "sprintf-js": ["1.1.3"], "sql.js": ["1.14.1"], + "standardwebhooks": ["1.1.1"], "stream-browserify": ["3.0.0"], "string-width": ["4.2.3"], "string_decoder": ["1.1.1"], @@ -709,6 +716,7 @@ export const EMBEDDED_NPM_PACKAGES: Readonly> "tr46": ["6.0.0"], "trim-lines": ["3.0.1"], "trough": ["2.2.0"], + "ts-algebra": ["2.0.0"], "tslib": ["1.14.1", "2.8.1"], "tunnel-agent": ["0.6.0"], "turndown": ["7.2.4"], @@ -981,6 +989,7 @@ export const PROXY_EMBEDDED_NPM_PACKAGES: Readonly> = { + "@anthropic-ai/sdk": ["0.128.0"], "@aws-sdk/client-s3": ["3.980.0"], "@aws-sdk/lib-storage": ["3.980.0"], "@babel/generator": ["7.29.1"], @@ -1048,6 +1057,7 @@ export const EMBEDDED_NPM_CONSTRAINTS: Readonly Date: Mon, 28 Sep 2026 23:48:25 +0200 Subject: [PATCH 06/13] Prove provider docs against the SDKs users install The provider guide examples need to stay runnable against the official OpenAI and Anthropic clients without adding those docs-only SDKs to the repository lockfile or embedded runtime package map. The docs test now executes the extracted snippets in a temporary Deno project with its own lockfile, pinned npm SDK imports, isolated environment, and captured fetch transport. Constraint: Review requires exercising official SDK constructor and request behavior, while docs-only SDK packages must not enter the root production lockfile or embedded runtime package map. Rejected: Keep local lookalike clients | they prove the Veryfront protocol shape but not SDK compatibility. Rejected: Import SDKs in the root docs test module | it reintroduces docs-only packages into root dependency metadata. Confidence: high Scope-risk: narrow Reversibility: clean Tested: deno task test:file tests/integration/docs/provider-client-examples.test.ts Tested: deno fmt --check tests/integration/docs/provider-client-examples.test.ts Tested: deno check --config=scripts/test.deno.json tests/integration/docs/provider-client-examples.test.ts Tested: codex review --uncommitted Not-tested: Live Veryfront gateway or vendor network calls; fetch is intentionally captured locally. --- .../docs/provider-client-examples.test.ts | 162 +++++++++++------- 1 file changed, 104 insertions(+), 58 deletions(-) diff --git a/tests/integration/docs/provider-client-examples.test.ts b/tests/integration/docs/provider-client-examples.test.ts index 1c1393b0cc..10506c540c 100644 --- a/tests/integration/docs/provider-client-examples.test.ts +++ b/tests/integration/docs/provider-client-examples.test.ts @@ -1,61 +1,117 @@ import { assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; -type ClientOptions = { - baseURL: string; - apiKey?: string; - authToken?: string; +type RecordedRequest = { + url: string; + method: string; + headers: Record; + body: unknown; }; -class OpenAIClient { - readonly chat = { - completions: { - create: (body: unknown) => this.request("/chat/completions", body), - }, - }; - - constructor(private readonly options: ClientOptions) {} +async function runSnippetsWithOfficialClients( + snippets: string[], +): Promise { + const tempDir = await Deno.makeTempDir({ prefix: "vf-provider-docs-" }); + try { + const modulePath = `${tempDir}/provider-snippets.ts`; + const lockPath = `${tempDir}/deno.lock`; + const configPath = `${tempDir}/deno.json`; + await Deno.writeTextFile( + configPath, + JSON.stringify( + { + lock: lockPath, + nodeModulesDir: "none", + }, + null, + 2, + ), + ); + await Deno.writeTextFile(modulePath, buildSnippetModule(snippets)); - private request(path: string, body: unknown): Promise { - return recordRequest(`${this.options.baseURL}${path}`, { - method: "POST", - headers: { authorization: `Bearer ${this.options.apiKey}` }, - body: JSON.stringify(body), + const command = new Deno.Command(Deno.execPath(), { + args: [ + "run", + "--quiet", + "--config", + configPath, + "--allow-env", + modulePath, + ], + clearEnv: true, + env: { VERYFRONT_API_KEY: "example-project-key" }, + stdout: "piped", + stderr: "piped", }); + const output = await command.output(); + const decoder = new TextDecoder(); + const stdout = decoder.decode(output.stdout); + const stderr = decoder.decode(output.stderr); + if (!output.success) { + throw new Error( + `Provider snippet subprocess failed with ${output.code}:\n${stderr}\n${stdout}`, + ); + } + return JSON.parse(stdout) as RecordedRequest[]; + } finally { + await Deno.remove(tempDir, { recursive: true }); } } -class AnthropicClient { - readonly messages = { - create: (body: unknown) => this.request("/v1/messages", body), - }; +function buildSnippetModule(snippets: string[]): string { + const transformedSnippets = snippets.map((snippet) => + snippet + .replace( + 'import OpenAI from "openai";', + 'import OpenAI from "npm:openai@7.23.0";', + ) + .replace( + 'import Anthropic from "@anthropic-ai/sdk";', + 'import Anthropic from "npm:@anthropic-ai/sdk@0.128.0";', + ) + ); + const imports = transformedSnippets.map((snippet) => snippet.split("\n")[0]); + const bodies = transformedSnippets.map((snippet) => snippet.split("\n").slice(1).join("\n")); - constructor(private readonly options: ClientOptions) {} + return ` +${imports.join("\n")} - private request(path: string, body: unknown): Promise { - return recordRequest(`${this.options.baseURL}${path}`, { - method: "POST", - headers: { - authorization: `Bearer ${this.options.authToken}`, - "anthropic-version": "2023-06-01", - }, - body: JSON.stringify(body), - }); - } -} +const requests = []; +const encoder = new TextEncoder(); +globalThis.fetch = async (input, init = {}) => { + const request = new Request(input, init); + requests.push({ + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + body: request.body ? JSON.parse(await request.text()) : null, + }); + const body = request.url.endsWith("/chat/completions") + ? { id: "chatcmpl_example", object: "chat.completion", choices: [] } + : { + id: "msg_example", + type: "message", + role: "assistant", + model: "anthropic/claude-sonnet-4-6", + content: [], + stop_reason: "end_turn", + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + }; + return new Response(encoder.encode(JSON.stringify(body)), { + status: 200, + headers: { "content-type": "application/json" }, + }); +}; -const requests: Request[] = []; +${bodies.map((body) => `{\n${body}\n}`).join("\n\n")} -function recordRequest(input: string, init: RequestInit): Promise { - requests.push(new Request(input, init)); - return Promise.resolve( - Response.json({ id: "example", content: [], choices: [] }), - ); +console.log(JSON.stringify(requests)); +`; } describe("provider guide client snippets", () => { it("sends the documented gateway requests", async () => { - requests.length = 0; const guide = await Deno.readTextFile( new URL("../../../docs/guides/providers.md", import.meta.url), ); @@ -67,19 +123,9 @@ describe("provider guide client snippets", () => { 'import OpenAI from "openai";', 'import Anthropic from "@anthropic-ai/sdk";', ]); - for (const snippet of snippets) { - const executable = snippet.replace(/^import .*;\n/gm, ""); - await new Function( - "OpenAI", - "Anthropic", - "process", - `return (async () => {\n${executable}\n})();`, - )( - OpenAIClient, - AnthropicClient, - { env: { VERYFRONT_API_KEY: "example-project-key" } }, - ); - } + + const requests = await runSnippetsWithOfficialClients(snippets); + assertEquals(requests.length, 2); assertEquals(requests.map((request) => request.method), ["POST", "POST"]); assertEquals(requests.map((request) => request.url), [ @@ -87,19 +133,19 @@ describe("provider guide client snippets", () => { "https://api.veryfront.com/ai/v1/messages", ]); assertEquals( - requests[0]!.headers.get("authorization"), + requests[0]!.headers.authorization, "Bearer example-project-key", ); assertEquals( - requests[1]!.headers.get("authorization"), + requests[1]!.headers.authorization, "Bearer example-project-key", ); - assertEquals(requests[1]!.headers.get("anthropic-version"), "2023-06-01"); - assertEquals(await requests[0]!.json(), { + assertEquals(requests[1]!.headers["anthropic-version"], "2023-06-01"); + assertEquals(requests[0]!.body, { model: "mistral/mistral-small-2503", messages: [{ role: "user", content: "Say hello." }], }); - assertEquals(await requests[1]!.json(), { + assertEquals(requests[1]!.body, { model: "anthropic/claude-sonnet-4-6", max_tokens: 1024, messages: [{ role: "user", content: "Say hello." }], From 14765087d220554dd3cdda7dec0cb399fdd8abe9 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 29 Sep 2026 00:12:47 +0200 Subject: [PATCH 07/13] Keep provider example validation reproducible across dependency releases Use the shared test filesystem adapter and an isolated frozen SDK lockfile without adding runtime dependencies. Confidence: high Scope-risk: narrow Tested: Official SDK request smoke test; direct typecheck and lint; testing front-door audit; codex review --uncommitted --- .../docs/provider-client-examples.deno.lock | 46 +++++++++++++++++++ .../docs/provider-client-examples.test.ts | 8 +++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/integration/docs/provider-client-examples.deno.lock diff --git a/tests/integration/docs/provider-client-examples.deno.lock b/tests/integration/docs/provider-client-examples.deno.lock new file mode 100644 index 0000000000..f006384a39 --- /dev/null +++ b/tests/integration/docs/provider-client-examples.deno.lock @@ -0,0 +1,46 @@ +{ + "version": "5", + "specifiers": { + "npm:@anthropic-ai/sdk@0.128.0": "0.128.0", + "npm:openai@7.23.0": "7.23.0" + }, + "npm": { + "@anthropic-ai/sdk@0.128.0": { + "integrity": "sha512-tl5cBFZC1jVFrTuQyvQObA4WmuNgcYVeXfriqOumgtLAHUm4gPUj18YnszBW60v0PrjDB8nBDOC6wd9CmHQFlA==", + "dependencies": [ + "json-schema-to-ts", + "standardwebhooks" + ], + "bin": true + }, + "@babel/runtime@7.29.7": { + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==" + }, + "@stablelib/base64@1.0.1": { + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, + "fast-sha256@1.3.0": { + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, + "json-schema-to-ts@3.1.1": { + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dependencies": [ + "@babel/runtime", + "ts-algebra" + ] + }, + "openai@7.23.0": { + "integrity": "sha512-0ecOXnFSMNWZq6cUBcTV6Lf93y+fm8BH/+QzFvpoP1UGNOE5pnytRa6HAPGCw+3IS9SIAW7rc1M8yyzN1PiBAQ==" + }, + "standardwebhooks@1.1.1": { + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "dependencies": [ + "@stablelib/base64", + "fast-sha256" + ] + }, + "ts-algebra@2.0.0": { + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + } + } +} diff --git a/tests/integration/docs/provider-client-examples.test.ts b/tests/integration/docs/provider-client-examples.test.ts index 10506c540c..49924bbeea 100644 --- a/tests/integration/docs/provider-client-examples.test.ts +++ b/tests/integration/docs/provider-client-examples.test.ts @@ -1,5 +1,6 @@ import { assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; +import { makeTempDir } from "#veryfront/testing/deno-compat.ts"; type RecordedRequest = { url: string; @@ -11,10 +12,14 @@ type RecordedRequest = { async function runSnippetsWithOfficialClients( snippets: string[], ): Promise { - const tempDir = await Deno.makeTempDir({ prefix: "vf-provider-docs-" }); + const tempDir = await makeTempDir({ prefix: "vf-provider-docs-" }); try { const modulePath = `${tempDir}/provider-snippets.ts`; const lockPath = `${tempDir}/deno.lock`; + await Deno.copyFile( + new URL("./provider-client-examples.deno.lock", import.meta.url), + lockPath, + ); const configPath = `${tempDir}/deno.json`; await Deno.writeTextFile( configPath, @@ -33,6 +38,7 @@ async function runSnippetsWithOfficialClients( args: [ "run", "--quiet", + "--frozen", "--config", configPath, "--allow-env", From 25cbcf2aa56c2972d43832d3b586a9f67db81755 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 29 Sep 2026 01:32:58 +0200 Subject: [PATCH 08/13] Prevent inherited vendor credentials in gateway examples The official Anthropic client reads its vendor API key from the environment even when the example supplies a gateway bearer token. Explicitly disable that key, and exercise the real client with a harmless vendor-key fixture. Preserve the configured Deno cache in the isolated subprocess without inheriting host credentials. Constraint: Documentation examples must send only the project gateway credential Rejected: Clear all vendor keys on the caller host | examples must work without changing unrelated credentials Confidence: high Scope-risk: narrow Tested: Regression failed before apiKey null; official SDK test passed on pinned Deno in macOS and Linux; 44 guide tests and 71 steps passed Directive: Keep the SDK lock isolated from production dependencies --- docs/guides/providers.md | 1 + tests/integration/docs/provider-client-examples.test.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/guides/providers.md b/docs/guides/providers.md index fa58c7ff46..3dba47855b 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -117,6 +117,7 @@ import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.veryfront.com/ai", + apiKey: null, authToken: process.env.VERYFRONT_API_KEY, }); diff --git a/tests/integration/docs/provider-client-examples.test.ts b/tests/integration/docs/provider-client-examples.test.ts index 49924bbeea..e78963dbbe 100644 --- a/tests/integration/docs/provider-client-examples.test.ts +++ b/tests/integration/docs/provider-client-examples.test.ts @@ -34,6 +34,7 @@ async function runSnippetsWithOfficialClients( ); await Deno.writeTextFile(modulePath, buildSnippetModule(snippets)); + const denoCacheDirectory = Deno.env.get("DENO_DIR"); const command = new Deno.Command(Deno.execPath(), { args: [ "run", @@ -45,7 +46,11 @@ async function runSnippetsWithOfficialClients( modulePath, ], clearEnv: true, - env: { VERYFRONT_API_KEY: "example-project-key" }, + env: { + ...(denoCacheDirectory === undefined ? {} : { DENO_DIR: denoCacheDirectory }), + VERYFRONT_API_KEY: "example-project-key", + ANTHROPIC_API_KEY: "vendor-key-must-not-be-sent", + }, stdout: "piped", stderr: "piped", }); @@ -147,6 +152,7 @@ describe("provider guide client snippets", () => { "Bearer example-project-key", ); assertEquals(requests[1]!.headers["anthropic-version"], "2023-06-01"); + assertEquals(requests[1]!.headers["x-api-key"], undefined); assertEquals(requests[0]!.body, { model: "mistral/mistral-small-2503", messages: [{ role: "user", content: "Say hello." }], From 76e0891d468bfea64a94b1cb2a7393bdcded063c Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 29 Sep 2026 01:41:11 +0200 Subject: [PATCH 09/13] Keep push conflict verification independent of directory order The Linux verification run enumerates source files in a different order from the host filesystem. The conflict test concerns every attempted write and refused receipt, not the directory order. Compare the exact path multiset while retaining all write counts, conflict checks, and receipt assertions. Constraint: Directory iteration order is unspecified across filesystems Confidence: high Scope-risk: narrow Tested: Linux pinned-Deno push command suite passed 15 tests and 144 steps; uncommitted review found no regression Directive: Do not make this conflict assertion depend on filesystem iteration order --- cli/commands/push/command.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/commands/push/command.test.ts b/cli/commands/push/command.test.ts index 364c2e581e..35eac636f9 100644 --- a/cli/commands/push/command.test.ts +++ b/cli/commands/push/command.test.ts @@ -3789,11 +3789,12 @@ describe("push divergence guard", () => { assertEquals((error as Error & { slug?: string }).slug, "push-conflict"); assertStringIncludes(error.message, '"app.ts"'); assertEquals(fileListCalls, 3); - assertEquals(putPaths, [ + // Directory iteration order varies by filesystem; verify every attempted write. + assertEquals(putPaths.toSorted(), [ "/projects/my-project/files/app.ts", - "/projects/my-project/files/second.ts", "/projects/my-project/files/app.ts", "/projects/my-project/files/second.ts", + "/projects/my-project/files/second.ts", ]); assertEquals(await readPushReceipt(projectDir), null); assertEquals( From b6b77eb7edf7e352a2d285b5a8106f6b957447d3 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Tue, 29 Sep 2026 02:56:33 +0200 Subject: [PATCH 10/13] test(docs): use provider alias in guide assertions --- tests/docs/guide-code-examples.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docs/guide-code-examples.test.ts b/tests/docs/guide-code-examples.test.ts index 8495ce929f..165dbadb1c 100644 --- a/tests/docs/guide-code-examples.test.ts +++ b/tests/docs/guide-code-examples.test.ts @@ -78,7 +78,7 @@ import { parsePushArgs } from "../../cli/commands/push/command.ts"; import { parseCliArgs } from "../../cli/shared/args.ts"; import { AUTH_PRESETS } from "../../cli/scaffold/engine.ts"; import { getTemplate } from "../../templates/index.ts"; -import { getVeryfrontCloudGatewayBaseUrl } from "../../src/provider/veryfront-cloud/shared.ts"; +import { getVeryfrontCloudGatewayBaseUrl } from "#veryfront/provider/veryfront-cloud/shared.ts"; const EXISTING_GUIDE_EXAMPLE_SUITE = [ "agents.md", From e2b10bab72c6e3d3fe15d6d887fceef682b017b3 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Tue, 29 Sep 2026 02:57:41 +0200 Subject: [PATCH 11/13] docs(providers): use the served model catalog endpoint --- docs/guides/providers.md | 2 +- tests/docs/guide-code-examples.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/providers.md b/docs/guides/providers.md index 3dba47855b..24b5e0d226 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -73,7 +73,7 @@ The Veryfront CLI and SDK read `VERYFRONT_API_TOKEN`, described above. Name the model as `/`. List the models your key can use: ```bash -curl https://api.veryfront.com/ai/v1/models \ +curl https://api.veryfront.com/ai/models \ -H "Authorization: Bearer $VERYFRONT_API_KEY" ``` diff --git a/tests/docs/guide-code-examples.test.ts b/tests/docs/guide-code-examples.test.ts index 165dbadb1c..15b8cfc164 100644 --- a/tests/docs/guide-code-examples.test.ts +++ b/tests/docs/guide-code-examples.test.ts @@ -250,7 +250,7 @@ describe("Guide: providers.md", () => { assertEquals(await neutralBaseUrl("acme-labs"), `${api}/ai/v1`); assertStringIncludes(section, "`/`"); - assertStringIncludes(section, `curl ${api}/ai/v1/models`); + assertStringIncludes(section, `curl ${api}/ai/models`); }); }); From 906f47994c3b4ba4df546d0a472cba6aa17747b3 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Tue, 29 Sep 2026 03:07:47 +0200 Subject: [PATCH 12/13] docs(providers): reject missing Veryfront API key --- docs/guides/providers.md | 7 ++++- .../docs/provider-client-examples.test.ts | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/guides/providers.md b/docs/guides/providers.md index 24b5e0d226..dfaee46089 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -91,9 +91,14 @@ built-in entry for: ```ts import OpenAI from "openai"; +const projectApiKey = process.env.VERYFRONT_API_KEY; +if (!projectApiKey) { + throw new Error("Set VERYFRONT_API_KEY before running this example."); +} + const client = new OpenAI({ baseURL: "https://api.veryfront.com/ai/v1", - apiKey: process.env.VERYFRONT_API_KEY, + apiKey: projectApiKey, }); const completion = await client.chat.completions.create({ diff --git a/tests/integration/docs/provider-client-examples.test.ts b/tests/integration/docs/provider-client-examples.test.ts index e78963dbbe..1780dce976 100644 --- a/tests/integration/docs/provider-client-examples.test.ts +++ b/tests/integration/docs/provider-client-examples.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { makeTempDir } from "#veryfront/testing/deno-compat.ts"; @@ -11,6 +11,7 @@ type RecordedRequest = { async function runSnippetsWithOfficialClients( snippets: string[], + options: { includeVeryfrontApiKey?: boolean } = {}, ): Promise { const tempDir = await makeTempDir({ prefix: "vf-provider-docs-" }); try { @@ -48,7 +49,10 @@ async function runSnippetsWithOfficialClients( clearEnv: true, env: { ...(denoCacheDirectory === undefined ? {} : { DENO_DIR: denoCacheDirectory }), - VERYFRONT_API_KEY: "example-project-key", + ...(options.includeVeryfrontApiKey === false + ? {} + : { VERYFRONT_API_KEY: "example-project-key" }), + OPENAI_API_KEY: "openai-vendor-key-must-not-be-sent", ANTHROPIC_API_KEY: "vendor-key-must-not-be-sent", }, stdout: "piped", @@ -122,13 +126,17 @@ console.log(JSON.stringify(requests)); } describe("provider guide client snippets", () => { - it("sends the documented gateway requests", async () => { + async function getSnippets(): Promise { const guide = await Deno.readTextFile( new URL("../../../docs/guides/providers.md", import.meta.url), ); const section = guide.split("### Call the AI Gateway from other clients")[1]! .split("\n## ")[0]!; - const snippets = [...section.matchAll(/```ts\n([\s\S]*?)```/g)].map((match) => match[1]!); + return [...section.matchAll(/```ts\n([\s\S]*?)```/g)].map((match) => match[1]!); + } + + it("sends the documented gateway requests", async () => { + const snippets = await getSnippets(); assertEquals(snippets.length, 2); assertEquals(snippets.map((snippet) => snippet.split("\n")[0]), [ 'import OpenAI from "openai";', @@ -163,4 +171,14 @@ describe("provider guide client snippets", () => { messages: [{ role: "user", content: "Say hello." }], }); }); + + it("requires a Veryfront key instead of falling back to a vendor key", async () => { + const error = await assertRejects( + async () => + runSnippetsWithOfficialClients(await getSnippets(), { includeVeryfrontApiKey: false }), + Error, + ); + + assertEquals(error.message.includes("Set VERYFRONT_API_KEY"), true); + }); }); From a330e9586216843223f243476e66adee9b4a660c Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Tue, 29 Sep 2026 03:16:43 +0200 Subject: [PATCH 13/13] docs(providers): close API key and model example gaps --- docs/guides/providers.md | 6 ++++++ tests/docs/guide-code-examples.test.ts | 8 +++++++- .../docs/provider-client-examples.test.ts | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/guides/providers.md b/docs/guides/providers.md index dfaee46089..d652a55aff 100644 --- a/docs/guides/providers.md +++ b/docs/guides/providers.md @@ -70,6 +70,12 @@ is refused. The key starts with `vf_` and is bound to its project. The examples read it from `VERYFRONT_API_KEY`, a name for your own code only. The Veryfront CLI and SDK read `VERYFRONT_API_TOKEN`, described above. +Set the project API key in your shell before running the examples: + +```bash +export VERYFRONT_API_KEY="" +``` + Name the model as `/`. List the models your key can use: ```bash diff --git a/tests/docs/guide-code-examples.test.ts b/tests/docs/guide-code-examples.test.ts index 15b8cfc164..608afef7e1 100644 --- a/tests/docs/guide-code-examples.test.ts +++ b/tests/docs/guide-code-examples.test.ts @@ -218,6 +218,12 @@ describe("Guide: providers.md", () => { return guide.slice(start, end === -1 ? undefined : end); } + it("shows how to export a project key for the client examples", async () => { + const section = await gatewayClientSection(); + + assertStringIncludes(section, 'export VERYFRONT_API_KEY=""'); + }); + // The guide documents the default routes, so a host that opts back into // vendor routes must not change what this suite compares against. function neutralBaseUrl(provider: string): Promise { @@ -245,7 +251,7 @@ describe("Guide: providers.md", () => { assertStringIncludes(section, `curl ${base}/messages`); }); - it("sends a vendor the SDK does not know over the OpenAI protocol", async () => { + it("maps unknown SDK vendors to the OpenAI protocol and documents open provider ids", async () => { const section = await gatewayClientSection(); assertEquals(await neutralBaseUrl("acme-labs"), `${api}/ai/v1`); diff --git a/tests/integration/docs/provider-client-examples.test.ts b/tests/integration/docs/provider-client-examples.test.ts index 1780dce976..bf0655ad68 100644 --- a/tests/integration/docs/provider-client-examples.test.ts +++ b/tests/integration/docs/provider-client-examples.test.ts @@ -172,6 +172,22 @@ describe("provider guide client snippets", () => { }); }); + it("passes an unknown vendor model id through the OpenAI client", async () => { + const [openAiSnippet] = await getSnippets(); + const unknownVendorSnippet = openAiSnippet!.replace( + "mistral/mistral-small-2503", + "acme-labs/model-not-in-the-built-in-catalog", + ); + + const [request] = await runSnippetsWithOfficialClients([unknownVendorSnippet]); + + assertEquals(request!.url, "https://api.veryfront.com/ai/v1/chat/completions"); + assertEquals( + (request!.body as { model: string }).model, + "acme-labs/model-not-in-the-built-in-catalog", + ); + }); + it("requires a Veryfront key instead of falling back to a vendor key", async () => { const error = await assertRejects( async () => @@ -179,6 +195,9 @@ describe("provider guide client snippets", () => { Error, ); + if (!(error instanceof Error)) { + throw new Error("Expected the missing-project-key guard to reject with an Error"); + } assertEquals(error.message.includes("Set VERYFRONT_API_KEY"), true); }); });