From f98ab855fc7f08ec7005db1563672f5ebbb1cb25 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 27 Sep 2026 16:15:36 +0200 Subject: [PATCH 1/2] fix(agent): follow the served surface for tool schema rules and replay A Veryfront Cloud provider that only the served catalog lists takes the tool schema rules of the surface it is served on (Google or Anthropic), and a provider served on the Anthropic surface replays thinking and tool blocks like anthropic/*. Provider-hosted tools (web search, web fetch) still follow the vendor, since a third-party model cannot run them. The replay provider mapping moves to provider-replay-protocol.ts so it can be tested directly. --- docs/api-reference/veryfront/provider.md | 2 +- src/agent/runtime/index.ts | 47 +++------------- .../runtime/provider-replay-protocol.test.ts | 52 ++++++++++++++++++ src/agent/runtime/provider-replay-protocol.ts | 54 +++++++++++++++++++ .../runtime/provider-tool-compat.test.ts | 43 ++++++++++++++- src/agent/runtime/provider-tool-compat.ts | 10 ++++ src/provider/veryfront-cloud/model-catalog.ts | 17 ++++-- 7 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 src/agent/runtime/provider-replay-protocol.test.ts create mode 100644 src/agent/runtime/provider-replay-protocol.ts diff --git a/docs/api-reference/veryfront/provider.md b/docs/api-reference/veryfront/provider.md index f635d54e5f..f90ab5997e 100644 --- a/docs/api-reference/veryfront/provider.md +++ b/docs/api-reference/veryfront/provider.md @@ -91,7 +91,7 @@ Clear all registered model providers and reset lazy built-ins (for testing). | `resolveVeryfrontCloudModelThinking` | Resolves Veryfront Cloud model thinking. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts) | | `resolveVeryfrontCloudReasoningOption` | Resolves provider-neutral runtime reasoning for a Veryfront Cloud model. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts) | | `resolveVeryfrontCloudThinkingProviderOptions` | Options accepted by resolve Veryfront Cloud thinking provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts) | -| `tryGetVeryfrontCloudProviderFromModelId` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts) | +| `tryGetVeryfrontCloudProviderFromModelId` | Return the Veryfront Cloud provider named by a model ID, including one this package does not list, or `undefined` when the ID names none. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts) | ### Types diff --git a/src/agent/runtime/index.ts b/src/agent/runtime/index.ts index d4f2cbf3f9..af630612d5 100644 --- a/src/agent/runtime/index.ts +++ b/src/agent/runtime/index.ts @@ -115,13 +115,15 @@ import { import { tryGetCacheKeyContext } from "#veryfront/cache/cache-key-builder.ts"; import type { ToolExecutionContext } from "#veryfront/tool"; import { - getModelRuntimeId, - getModelRuntimeProvider, isLocalModelRuntime, supportsModelRuntimeToolCalling, } from "#veryfront/provider/runtime-inspection.ts"; import { generateText, streamText } from "#veryfront/runtime/runtime-bridge.ts"; import { resolveAgentSystem } from "./effective-agent-system.ts"; +import { + resolveActiveProviderReplayProvider, + resolveRuntimeGenAiProviderName, +} from "./provider-replay-protocol.ts"; import { attachOutputSchemaParser, resolveAgentOutputSchema, @@ -211,7 +213,6 @@ import { createProviderReplayCheckpointEmissionState, type ProviderReplayCheckpoint, type ProviderReplayCheckpointEmissionState, - type ProviderReplayProvider, } from "./provider-replay.ts"; import { applySourceIntegrationPolicy, @@ -299,42 +300,6 @@ export { const NativeError = Error; -function getActiveProviderReplayProvider( - languageModel: ModelRuntime, -): ProviderReplayProvider | "unsupported" { - const modelRuntimeId = getModelRuntimeId(languageModel); - const provider = - (typeof languageModel.modelProvider === "string" ? languageModel.modelProvider : undefined) ?? - getModelRuntimeProvider(languageModel) ?? - (modelRuntimeId !== undefined - ? resolveRuntimeGenAiProviderName(modelRuntimeId) ?? modelRuntimeId.split("/")[0] - : undefined); - if (provider === "anthropic") return "anthropic"; - if (provider === "openai") return "openai-responses"; - return "unsupported"; -} - -function resolveRuntimeGenAiProviderName(modelId: string): string | undefined { - const normalizedModelId = modelId.startsWith("veryfront-cloud/") - ? modelId.slice("veryfront-cloud/".length) - : modelId; - const provider = normalizedModelId.split("/")[0]?.trim().toLowerCase(); - - switch (provider) { - case "anthropic": - return "anthropic"; - case "openai": - return "openai"; - case "google": - case "google-ai-studio": - return "gcp.gen_ai"; - case "moonshotai": - return "moonshotai"; - default: - return undefined; - } -} - export { enforceSkillPolicy, type SkillPolicyResult } from "./skill-policy-enforcement.ts"; import { DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE, getModelMaxOutputTokens } from "./constants.ts"; @@ -2828,7 +2793,7 @@ export class AgentRuntime { applyProviderReplayCheckpointsToMessages( currentMessages, getRuntimeProviderReplayCheckpoints(this.config), - { activeProvider: getActiveProviderReplayProvider(languageModel) }, + { activeProvider: resolveActiveProviderReplayProvider(languageModel) }, ); const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }; @@ -3537,7 +3502,7 @@ export class AgentRuntime { applyProviderReplayCheckpointsToMessages( currentMessages, getRuntimeProviderReplayCheckpoints(this.config), - { activeProvider: getActiveProviderReplayProvider(languageModel) }, + { activeProvider: resolveActiveProviderReplayProvider(languageModel) }, ); const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }; diff --git a/src/agent/runtime/provider-replay-protocol.test.ts b/src/agent/runtime/provider-replay-protocol.test.ts new file mode 100644 index 0000000000..14118bccea --- /dev/null +++ b/src/agent/runtime/provider-replay-protocol.test.ts @@ -0,0 +1,52 @@ +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import type { ModelRuntime } from "#veryfront/provider/types.ts"; +import { registerVeryfrontCloudModelFacts } from "#veryfront/provider/veryfront-cloud/model-catalog.ts"; +import { + resolveActiveProviderReplayProvider, + resolveRuntimeGenAiProviderName, +} from "./provider-replay-protocol.ts"; + +function model(modelProvider: string, surface?: string): ModelRuntime { + const runtime: ModelRuntime = { + provider: "veryfront-cloud", + modelProvider, + modelId: "m1", + doGenerate: () => Promise.reject(new Error("not called")), + doStream: () => Promise.reject(new Error("not called")), + }; + if (surface !== undefined) { + registerVeryfrontCloudModelFacts(runtime, () => + ({ + provider: modelProvider, + surface, + native: false, + transportPlan: "chat-completions", + }) as never); + } + return runtime; +} + +describe("resolveActiveProviderReplayProvider", () => { + it("replays the named protocol providers as before", () => { + assertEquals(resolveActiveProviderReplayProvider(model("anthropic")), "anthropic"); + assertEquals(resolveActiveProviderReplayProvider(model("openai")), "openai-responses"); + }); + + it("replays a provider served on the Anthropic surface like anthropic/*", () => { + assertEquals(resolveActiveProviderReplayProvider(model("acme-labs", "anthropic")), "anthropic"); + }); + + it("does not replay a provider on another surface, or one with no served facts", () => { + assertEquals(resolveActiveProviderReplayProvider(model("acme-labs", "openai")), "unsupported"); + assertEquals(resolveActiveProviderReplayProvider(model("acme-labs")), "unsupported"); + }); +}); + +describe("resolveRuntimeGenAiProviderName", () => { + it("names the GenAI provider of a runtime model id", () => { + assertEquals(resolveRuntimeGenAiProviderName("veryfront-cloud/anthropic/claude"), "anthropic"); + assertEquals(resolveRuntimeGenAiProviderName("google-ai-studio/gemini"), "gcp.gen_ai"); + assertEquals(resolveRuntimeGenAiProviderName("acme-labs/m1"), undefined); + }); +}); diff --git a/src/agent/runtime/provider-replay-protocol.ts b/src/agent/runtime/provider-replay-protocol.ts new file mode 100644 index 0000000000..b9b6c851cd --- /dev/null +++ b/src/agent/runtime/provider-replay-protocol.ts @@ -0,0 +1,54 @@ +/** + * Which provider-replay protocol, and which GenAI provider name, a runtime + * model maps to. A Veryfront Cloud model of a provider served on the Anthropic + * surface replays like `anthropic/*`: the replay format belongs to the wire + * protocol, not to the vendor. + */ +import { + getModelRuntimeId, + getModelRuntimeProvider, +} from "#veryfront/provider/runtime-inspection.ts"; +import type { ModelRuntime } from "#veryfront/provider/types.ts"; +import { readVeryfrontCloudModelFacts } from "#veryfront/provider/veryfront-cloud/model-catalog.ts"; +import type { ProviderReplayProvider } from "./provider-replay.ts"; + +/** The provider-replay protocol a model's turns are replayed with. */ +export function resolveActiveProviderReplayProvider( + languageModel: ModelRuntime, +): ProviderReplayProvider | "unsupported" { + const modelRuntimeId = getModelRuntimeId(languageModel); + const provider = + (typeof languageModel.modelProvider === "string" ? languageModel.modelProvider : undefined) ?? + getModelRuntimeProvider(languageModel) ?? + (modelRuntimeId !== undefined + ? resolveRuntimeGenAiProviderName(modelRuntimeId) ?? modelRuntimeId.split("/")[0] + : undefined); + if (provider === "anthropic") return "anthropic"; + if (provider === "openai") return "openai-responses"; + // A Veryfront Cloud model of a provider served on the Anthropic surface + // replays Anthropic thinking and tool blocks like `anthropic/*`. + if (readVeryfrontCloudModelFacts(languageModel)?.surface === "anthropic") return "anthropic"; + return "unsupported"; +} + +/** The GenAI semantic-convention provider name for a runtime model id. */ +export function resolveRuntimeGenAiProviderName(modelId: string): string | undefined { + const normalizedModelId = modelId.startsWith("veryfront-cloud/") + ? modelId.slice("veryfront-cloud/".length) + : modelId; + const provider = normalizedModelId.split("/")[0]?.trim().toLowerCase(); + + switch (provider) { + case "anthropic": + return "anthropic"; + case "openai": + return "openai"; + case "google": + case "google-ai-studio": + return "gcp.gen_ai"; + case "moonshotai": + return "moonshotai"; + default: + return undefined; + } +} diff --git a/src/agent/runtime/provider-tool-compat.test.ts b/src/agent/runtime/provider-tool-compat.test.ts index d98167a830..45a9f8a8f0 100644 --- a/src/agent/runtime/provider-tool-compat.test.ts +++ b/src/agent/runtime/provider-tool-compat.test.ts @@ -1,5 +1,9 @@ import { assertEquals, assertStrictEquals } from "#veryfront/testing/assert.ts"; -import { describe, it } from "#veryfront/testing/bdd.ts"; +import { afterEach, describe, it } from "#veryfront/testing/bdd.ts"; +import { + __resetVeryfrontCloudCatalogForTests, + __setVeryfrontCloudCatalogForTests, +} from "#veryfront/provider/veryfront-cloud/catalog-client.ts"; import type { ToolDefinition } from "#veryfront/tool"; import { createMoonshotSchemaExpansionBudget, @@ -571,3 +575,40 @@ describe("provider-tool-compat", () => { }); }); }); + +describe("getProviderToolProfile for providers only the served catalog lists", () => { + afterEach(__resetVeryfrontCloudCatalogForTests); + + const served = (surface: string) => + __setVeryfrontCloudCatalogForTests({ + models: [{ + id: "m1", + modelId: "acme-labs/m1", + provider: "acme-labs", + surface, + operations: [surface === "anthropic" ? "messages" : "chat-completions"], + aliases: [], + capabilities: {}, + }], + }); + + it("applies the schema rules of the surface the provider is served on", () => { + served("google"); + assertEquals(getProviderToolProfile("veryfront-cloud/acme-labs/m1"), { + provider: "google", + sanitizeSchema: true, + }); + served("anthropic"); + assertEquals(getProviderToolProfile("veryfront-cloud/acme-labs/m1"), { + provider: "anthropic", + sanitizeSchema: true, + }); + }); + + it("keeps an unlisted provider on the OpenAI surface, and a direct id, unknown", () => { + served("openai"); + assertEquals(getProviderToolProfile("veryfront-cloud/acme-labs/m1").provider, "unknown"); + served("anthropic"); + assertEquals(getProviderToolProfile("acme-labs/m1").provider, "unknown"); + }); +}); diff --git a/src/agent/runtime/provider-tool-compat.ts b/src/agent/runtime/provider-tool-compat.ts index 43869f3b21..e1414beb41 100644 --- a/src/agent/runtime/provider-tool-compat.ts +++ b/src/agent/runtime/provider-tool-compat.ts @@ -1,3 +1,4 @@ +import { resolveVeryfrontCloudModelSurface } from "#veryfront/provider/veryfront-cloud/model-catalog.ts"; import { privateJsonStringify } from "#veryfront/security/private-json.ts"; import type { ToolDefinition } from "#veryfront/tool"; import type { JsonSchema } from "#veryfront/tool/schema"; @@ -101,6 +102,15 @@ export function getProviderToolProfile(model?: string): ProviderToolProfile { return { provider: "moonshot", sanitizeSchema: true }; } + // A Veryfront Cloud provider this list does not name takes the schema rules + // of the wire protocol it is served on: Google and Anthropic constrain tool + // schemas by protocol, not by vendor. + if (parts[0] === "veryfront-cloud") { + const surface = resolveVeryfrontCloudModelSurface(normalized); + if (surface === "google") return { provider: "google", sanitizeSchema: true }; + if (surface === "anthropic") return { provider: "anthropic", sanitizeSchema: true }; + } + return { provider: "unknown", sanitizeSchema: false }; } diff --git a/src/provider/veryfront-cloud/model-catalog.ts b/src/provider/veryfront-cloud/model-catalog.ts index 110a44f836..864169eaf4 100644 --- a/src/provider/veryfront-cloud/model-catalog.ts +++ b/src/provider/veryfront-cloud/model-catalog.ts @@ -726,17 +726,28 @@ export function getVeryfrontCloudProviderFromModelId( }); } -/** Return the Veryfront Cloud provider named by a model ID, including one this package does not list, or `undefined` when the ID names none. */ +/** + * The wire surface a Veryfront Cloud model ID's provider is served on, or + * `undefined` when the ID names no provider. A newly served provider reports + * the surface the served catalog gives it, not only the protocol-named ones. + */ +export function resolveVeryfrontCloudModelSurface( + modelId: string, +): VeryfrontCloudSurfaceId | undefined { + const provider = tryGetVeryfrontCloudProviderFromModelId(modelId); + return provider === undefined ? undefined : resolveVeryfrontCloudSurface(provider); +} + /** * Whether a Veryfront Cloud model ID speaks the Anthropic protocol: its * provider is served on the Anthropic surface. A newly served provider on that * surface counts, not only `anthropic/*`. */ export function isVeryfrontCloudAnthropicSurfaceModel(modelId: string): boolean { - const provider = tryGetVeryfrontCloudProviderFromModelId(modelId); - return provider !== undefined && resolveVeryfrontCloudSurface(provider) === "anthropic"; + return resolveVeryfrontCloudModelSurface(modelId) === "anthropic"; } +/** Return the Veryfront Cloud provider named by a model ID, including one this package does not list, or `undefined` when the ID names none. */ export function tryGetVeryfrontCloudProviderFromModelId( modelId: string, ): VeryfrontCloudProviderId | undefined { From 5d98503a854b08ba67af5d3b24beb04c63370381 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 27 Sep 2026 16:23:56 +0200 Subject: [PATCH 2/2] fix(agent): apply the served surface before the kimi- model-name heuristic --- src/agent/runtime/provider-tool-compat.test.ts | 18 ++++++++++++++++++ src/agent/runtime/provider-tool-compat.ts | 9 +++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/agent/runtime/provider-tool-compat.test.ts b/src/agent/runtime/provider-tool-compat.test.ts index 45a9f8a8f0..607f71cefd 100644 --- a/src/agent/runtime/provider-tool-compat.test.ts +++ b/src/agent/runtime/provider-tool-compat.test.ts @@ -605,6 +605,24 @@ describe("getProviderToolProfile for providers only the served catalog lists", ( }); }); + it("applies the served surface before the kimi- model-name heuristic", () => { + __setVeryfrontCloudCatalogForTests({ + models: [{ + id: "kimi-1", + modelId: "acme-labs/kimi-1", + provider: "acme-labs", + surface: "google", + operations: ["chat-completions"], + aliases: [], + capabilities: {}, + }], + }); + assertEquals(getProviderToolProfile("veryfront-cloud/acme-labs/kimi-1").provider, "google"); + // The heuristic still applies where no served surface decides. + assertEquals(getProviderToolProfile("kimi-k2.6").provider, "moonshot"); + assertEquals(getProviderToolProfile("moonshotai/kimi-k2.6").provider, "moonshot"); + }); + it("keeps an unlisted provider on the OpenAI surface, and a direct id, unknown", () => { served("openai"); assertEquals(getProviderToolProfile("veryfront-cloud/acme-labs/m1").provider, "unknown"); diff --git a/src/agent/runtime/provider-tool-compat.ts b/src/agent/runtime/provider-tool-compat.ts index e1414beb41..075f5da4a0 100644 --- a/src/agent/runtime/provider-tool-compat.ts +++ b/src/agent/runtime/provider-tool-compat.ts @@ -98,19 +98,24 @@ export function getProviderToolProfile(model?: string): ProviderToolProfile { return { provider: "anthropic", sanitizeSchema: true }; } - if (provider === "moonshot" || provider === "moonshotai" || modelName?.startsWith("kimi-")) { + if (provider === "moonshot" || provider === "moonshotai") { return { provider: "moonshot", sanitizeSchema: true }; } // A Veryfront Cloud provider this list does not name takes the schema rules // of the wire protocol it is served on: Google and Anthropic constrain tool - // schemas by protocol, not by vendor. + // schemas by protocol, not by vendor. This runs before the model-name + // heuristic below, so a served provider's own naming cannot override it. if (parts[0] === "veryfront-cloud") { const surface = resolveVeryfrontCloudModelSurface(normalized); if (surface === "google") return { provider: "google", sanitizeSchema: true }; if (surface === "anthropic") return { provider: "anthropic", sanitizeSchema: true }; } + if (modelName?.startsWith("kimi-")) { + return { provider: "moonshot", sanitizeSchema: true }; + } + return { provider: "unknown", sanitizeSchema: false }; }