From b8229c1675c709b40b8393bf5a0eb5aaa4aec38d Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 27 Sep 2026 12:55:05 +0200 Subject: [PATCH] fix(agent): route gateway-served qwen models through Veryfront Cloud Hosted runs on qwen/qwen3.8-27b failed with 'Model provider "qwen" not registered' because the hosted provider list was a hand-kept set that predates Qwen. Derive it from the Veryfront Cloud catalog data (aliases, routing rows, chat model providers) and add qwen, which the gateway serves ahead of the shipped catalog snapshot, on the default OpenAI surface at the vendor-neutral /ai/v1 route. --- src/agent/runtime/model-resolution.test.ts | 48 +++++++++++++++++++ src/agent/runtime/model-resolution.ts | 19 ++++---- src/provider/veryfront-cloud/model-catalog.ts | 14 ++++++ src/provider/veryfront-cloud/provider.test.ts | 38 +++++++++++++++ 4 files changed, 111 insertions(+), 8 deletions(-) diff --git a/src/agent/runtime/model-resolution.test.ts b/src/agent/runtime/model-resolution.test.ts index dbea1154d3..e3cc6c7198 100644 --- a/src/agent/runtime/model-resolution.test.ts +++ b/src/agent/runtime/model-resolution.test.ts @@ -10,6 +10,7 @@ import { deleteEnv, setEnv } from "#veryfront/compat/process.ts"; import { afterEach, describe, it } from "#veryfront/testing/bdd.ts"; import { resolveVeryfrontCloudModelId, + VERYFRONT_CLOUD_CATALOG_PROVIDER_NAMES, VERYFRONT_CLOUD_CHAT_MODELS, } from "#veryfront/provider/veryfront-cloud/model-catalog.ts"; import { @@ -394,6 +395,53 @@ describe("agent/runtime/model-resolution", () => { ); }); + it("routes every catalog provider through veryfront-cloud when only hosted bootstrap is available", () => { + setEnv("VERYFRONT_API_TOKEN", "vf_test_runtime"); + setEnv("VERYFRONT_PROJECT_SLUG", "demo-project"); + + for (const provider of VERYFRONT_CLOUD_CATALOG_PROVIDER_NAMES) { + // Mistral model IDs are gated by the catalog, so it needs a listed one. + const modelId = provider === "mistral" ? "mistral-small-2503" : "model-x"; + assertEquals( + resolveRuntimeModel(`${provider}/${modelId}`), + `veryfront-cloud/${provider}/${modelId}`, + provider, + ); + } + }); + + it("routes every vendor the gateway catalog serves through veryfront-cloud (#1913)", () => { + // The vendors GET /ai/models lists. Qwen is served before the catalog + // snapshot in this package names it; a vendor missing here fails hosted + // runs with `Model provider "" not registered`. + setEnv("VERYFRONT_API_TOKEN", "vf_test_runtime"); + setEnv("VERYFRONT_PROJECT_SLUG", "demo-project"); + + assertEquals( + [ + "anthropic/claude-sonnet-4-6", + "openai/gpt-5-nano", + "google/gemini-3.5-flash", + "mistral/mistral-small-2503", + "deepseek/deepseek-v4-flash", + "qwen/qwen3.8-27b", + ].map((model) => resolveRuntimeModel(model)), + [ + "veryfront-cloud/anthropic/claude-sonnet-4-6", + "veryfront-cloud/openai/gpt-5-nano", + "veryfront-cloud/google/gemini-3.5-flash", + "veryfront-cloud/mistral/mistral-small-2503", + "veryfront-cloud/deepseek/deepseek-v4-flash", + "veryfront-cloud/qwen/qwen3.8-27b", + ], + ); + }); + + it("keeps a gateway-only provider unrouted without hosted bootstrap", () => { + setEnv("OPENAI_API_KEY", "sk-test"); + assertEquals(resolveRuntimeModel("qwen/qwen3.8-27b"), "qwen/qwen3.8-27b"); + }); + it("routes catalog Gemini, Mistral, and Kimi models through veryfront-cloud when only hosted bootstrap is available", () => { setEnv("VERYFRONT_API_TOKEN", "vf_test_runtime"); setEnv("VERYFRONT_PROJECT_SLUG", "demo-project"); diff --git a/src/agent/runtime/model-resolution.ts b/src/agent/runtime/model-resolution.ts index 715206e7b5..fbb1dbe190 100644 --- a/src/agent/runtime/model-resolution.ts +++ b/src/agent/runtime/model-resolution.ts @@ -8,6 +8,7 @@ import { createRetiredVeryfrontCloudModelError, findVeryfrontCloudModelByModelId, isRetiredVeryfrontCloudModelId, + VERYFRONT_CLOUD_CATALOG_PROVIDER_NAMES, } from "#veryfront/provider/veryfront-cloud/model-catalog.ts"; import { DEFAULT_MODEL_CREDENTIAL_MISMATCH, NOT_SUPPORTED } from "#veryfront/errors"; import { @@ -21,14 +22,16 @@ import { getModelRuntimeProvider } from "#veryfront/provider/runtime-inspection. export const AUTO_AGENT_MODEL = "auto"; export const DEFAULT_AGENT_MODEL = "openai/gpt-5-nano"; -const HOSTED_PROVIDER_NAMES = new Set([ - "deepseek", - "anthropic", - "google", - "google-ai-studio", - "mistral", - "moonshotai", - "openai", +/** + * Providers the gateway serves that the catalog snapshot shipped in this + * package does not list yet. Each routes on the default surface, which the + * gateway serves at the vendor-neutral `/ai/v1`. Drop an entry once + * `deno task generate:model-catalog` adds its provider to the snapshot. + */ +const GATEWAY_PROVIDERS_AHEAD_OF_CATALOG = ["qwen"] as const; +const HOSTED_PROVIDER_NAMES: ReadonlySet = new Set([ + ...VERYFRONT_CLOUD_CATALOG_PROVIDER_NAMES, + ...GATEWAY_PROVIDERS_AHEAD_OF_CATALOG, ]); const DIRECT_CREDENTIAL_PROVIDER_ALIASES = new Map([ ["google-ai-studio", "google"], diff --git a/src/provider/veryfront-cloud/model-catalog.ts b/src/provider/veryfront-cloud/model-catalog.ts index 8822d3bf47..15d16c2116 100644 --- a/src/provider/veryfront-cloud/model-catalog.ts +++ b/src/provider/veryfront-cloud/model-catalog.ts @@ -415,6 +415,20 @@ export const VERYFRONT_CLOUD_CHAT_MODELS: readonly VeryfrontCloudChatModel[] = O }), ); +/** + * Every provider name the catalog data routes: accepted aliases, providers + * with a routing row, and providers of a listed chat model. Runtime model + * resolution sends `/` for these through the gateway when no + * direct provider credential applies. + */ +export const VERYFRONT_CLOUD_CATALOG_PROVIDER_NAMES: readonly string[] = Object.freeze([ + ...new Set([ + ...VERYFRONT_CLOUD_PROVIDER_ALIASES.map(([alias]) => alias), + ...VERYFRONT_CLOUD_PROVIDER_ROUTING.map(([provider]) => provider), + ...VERYFRONT_CLOUD_CHAT_MODELS.map((model) => model.provider), + ]), +]); + const defaultVeryfrontCloudChatModel = VERYFRONT_CLOUD_CHAT_MODELS.find( (model) => model.id === DEFAULT_VERYFRONT_CLOUD_MODEL_ID, ); diff --git a/src/provider/veryfront-cloud/provider.test.ts b/src/provider/veryfront-cloud/provider.test.ts index 0f3a0d1e4b..3c1f8754dd 100644 --- a/src/provider/veryfront-cloud/provider.test.ts +++ b/src/provider/veryfront-cloud/provider.test.ts @@ -1330,6 +1330,44 @@ describe("provider/veryfront-cloud", () => { assertEquals(result.text, "Hello"); }); + it("sends a hosted qwen/qwen3.8-27b run to the vendor-neutral gateway route (#1913)", async () => { + setCloudBootstrap(); + const encoder = new TextEncoder(); + let captured: { url: string; model: unknown } | undefined; + + installMockFetch( + (async (input: URL | Request | string, init?: RequestInit) => { + const request = new Request(input, init); + captured = { url: request.url, model: JSON.parse(await request.text()).model }; + + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode('data: {"choices":[{"delta":{"content":"Hello"}}]}\n\n'), + ); + controller.enqueue( + encoder.encode('data: {"choices":[{"finish_reason":"stop"}]}\n\n'), + ); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } }, + ); + }) as typeof fetch, + ); + + const assistant = agent({ model: "qwen/qwen3.8-27b", system: "You are concise." }); + const result = await assistant.generate({ input: "Hi" }); + + assertEquals(captured, { + url: "https://api.veryfront.com/ai/v1/chat/completions", + model: "qwen/qwen3.8-27b", + }); + assertEquals(result.text, "Hello"); + }); + it("keeps an unlisted provider on chat completions for a reasoning-style model id", async () => { // "gpt-5.4" is a reasoning-style ID. Only the provider that implements the // OpenAI surface natively serves /responses, so an unlisted provider must