From 8601b63d0a23ff26c63547b62242e396e08f7f92 Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Wed, 15 Jul 2026 11:46:17 -0400 Subject: [PATCH 1/8] Move model capability tables into ai-config --- packages/ai-config/src/index.ts | 12 ++++ .../__tests__/anthropic-helpers.test.ts | 2 +- .../model-capabilities/anthropic-helpers.ts | 2 +- .../model-capabilities/deepseek-helpers.ts | 0 .../src/model-capabilities/gemini-helpers.ts | 60 +---------------- .../src/model-capabilities/gemma-helpers.ts | 2 +- .../src/model-capabilities/openai-helpers.ts | 2 +- .../src/model-capabilities/positai-helpers.ts | 6 +- packages/ai-config/src/types.ts | 16 +++++ .../src/__tests__/gemini-interactions.test.ts | 2 +- packages/ai-provider-bridge/src/index.ts | 19 +++--- .../model-capabilities/gemini-interactions.ts | 65 +++++++++++++++++++ .../src/model-clients/GeminiClient.ts | 2 +- .../src/positron/vscode-lm-models.ts | 10 +-- .../src/providers/anthropic-provider.ts | 3 +- .../src/providers/bedrock-provider.ts | 2 +- .../src/providers/deepseek-provider.ts | 3 +- .../src/providers/gemini-provider.ts | 7 +- .../src/providers/google-vertex-provider.ts | 2 +- .../src/providers/openai-provider.ts | 6 +- .../src/providers/openrouter-provider.ts | 3 +- .../src/providers/positai-provider.ts | 3 +- .../providers/snowflake-cortex-provider.ts | 4 +- 23 files changed, 135 insertions(+), 98 deletions(-) rename packages/{ai-provider-bridge => ai-config}/src/model-capabilities/__tests__/anthropic-helpers.test.ts (99%) rename packages/{ai-provider-bridge => ai-config}/src/model-capabilities/anthropic-helpers.ts (98%) rename packages/{ai-provider-bridge => ai-config}/src/model-capabilities/deepseek-helpers.ts (100%) rename packages/{ai-provider-bridge => ai-config}/src/model-capabilities/gemini-helpers.ts (66%) rename packages/{ai-provider-bridge => ai-config}/src/model-capabilities/gemma-helpers.ts (96%) rename packages/{ai-provider-bridge => ai-config}/src/model-capabilities/openai-helpers.ts (98%) rename packages/{ai-provider-bridge => ai-config}/src/model-capabilities/positai-helpers.ts (84%) create mode 100644 packages/ai-provider-bridge/src/model-capabilities/gemini-interactions.ts diff --git a/packages/ai-config/src/index.ts b/packages/ai-config/src/index.ts index 324e784..8b81760 100644 --- a/packages/ai-config/src/index.ts +++ b/packages/ai-config/src/index.ts @@ -61,8 +61,20 @@ export type { ResolvedModelInfo, ResolvedProvider, ResolvedProviderId, + InferredModelCapabilities, } from "./types.js"; +// --- Model capability inference --------------------------------------------- +export { getAnthropicModelCapabilities } from "./model-capabilities/anthropic-helpers.js"; +export { getDeepSeekModelCapabilities } from "./model-capabilities/deepseek-helpers.js"; +export { getGeminiModelCapabilities } from "./model-capabilities/gemini-helpers.js"; +export { getGemmaModelCapabilities } from "./model-capabilities/gemma-helpers.js"; +export { + getOpenAIModelCapabilities, + openaiMaxInputTokens, +} from "./model-capabilities/openai-helpers.js"; +export { getPositAiModelCapabilities } from "./model-capabilities/positai-helpers.js"; + // --- Defaults -------------------------------------------------------------- export { BEDROCK_DEFAULTS, diff --git a/packages/ai-provider-bridge/src/model-capabilities/__tests__/anthropic-helpers.test.ts b/packages/ai-config/src/model-capabilities/__tests__/anthropic-helpers.test.ts similarity index 99% rename from packages/ai-provider-bridge/src/model-capabilities/__tests__/anthropic-helpers.test.ts rename to packages/ai-config/src/model-capabilities/__tests__/anthropic-helpers.test.ts index b8f7bed..6b456ff 100644 --- a/packages/ai-provider-bridge/src/model-capabilities/__tests__/anthropic-helpers.test.ts +++ b/packages/ai-config/src/model-capabilities/__tests__/anthropic-helpers.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; -import { getAnthropicModelCapabilities } from "../anthropic-helpers"; +import { getAnthropicModelCapabilities } from "../anthropic-helpers.js"; describe("getAnthropicModelCapabilities", () => { describe("claude-opus-4-8", () => { diff --git a/packages/ai-provider-bridge/src/model-capabilities/anthropic-helpers.ts b/packages/ai-config/src/model-capabilities/anthropic-helpers.ts similarity index 98% rename from packages/ai-provider-bridge/src/model-capabilities/anthropic-helpers.ts rename to packages/ai-config/src/model-capabilities/anthropic-helpers.ts index b0a25ff..d4a2b73 100644 --- a/packages/ai-provider-bridge/src/model-capabilities/anthropic-helpers.ts +++ b/packages/ai-config/src/model-capabilities/anthropic-helpers.ts @@ -6,7 +6,7 @@ * Type guards and utilities for Anthropic provider metadata. */ -import type { ModelInfo } from "../types"; +import type { InferredModelCapabilities as ModelInfo } from "../types.js"; // --------------------------------------------------------------------------- // Anthropic model capability inference diff --git a/packages/ai-provider-bridge/src/model-capabilities/deepseek-helpers.ts b/packages/ai-config/src/model-capabilities/deepseek-helpers.ts similarity index 100% rename from packages/ai-provider-bridge/src/model-capabilities/deepseek-helpers.ts rename to packages/ai-config/src/model-capabilities/deepseek-helpers.ts diff --git a/packages/ai-provider-bridge/src/model-capabilities/gemini-helpers.ts b/packages/ai-config/src/model-capabilities/gemini-helpers.ts similarity index 66% rename from packages/ai-provider-bridge/src/model-capabilities/gemini-helpers.ts rename to packages/ai-config/src/model-capabilities/gemini-helpers.ts index bda6bc0..bba8ffb 100644 --- a/packages/ai-provider-bridge/src/model-capabilities/gemini-helpers.ts +++ b/packages/ai-config/src/model-capabilities/gemini-helpers.ts @@ -2,7 +2,7 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import type { ModelInfo } from "../types"; +import type { InferredModelCapabilities as ModelInfo } from "../types.js"; // --------------------------------------------------------------------------- // Gemini model capability inference @@ -163,61 +163,3 @@ export function getGeminiModelCapabilities(modelId: string): Partial thinkingEffortLevels: rule?.thinkingEffortLevels, }; } - -// --------------------------------------------------------------------------- -// Interactions API profile (eligibility + thinking) -// --------------------------------------------------------------------------- - -/** - * Profile for a model eligible for the Gemini Interactions API. - */ -export interface GeminiInteractionsProfile { - /** Valid wire-level thinkingLevel values for this model. */ - thinkingLevels: readonly string[]; -} - -/** - * Explicit enumerated allowlist of model IDs reachable via - * `POST /v1beta/interactions`. **Fail-closed**: unlisted IDs are excluded. - * - * To add a model: add its exact ID here with a profile, then update the - * corresponding CAPABILITY_RULES entry and tests. - * - * thinkingLevels must match the Interactions API docs for each model. - * The builder validates against these — levels not listed here are - * clamped to "medium". - */ -const INTERACTIONS_PROFILES: ReadonlyMap = new Map([ - // --- Gemini 2.5 (thinkingLevel: low/medium/high) --- - ["gemini-2.5-pro", { thinkingLevels: ["low", "medium", "high"] }], - ["gemini-2.5-flash", { thinkingLevels: ["low", "medium", "high"] }], - ["gemini-2.5-flash-lite", { thinkingLevels: ["low", "medium", "high"] }], - - // --- Gemini 3.x --- - ["gemini-3-flash-preview", { thinkingLevels: ["minimal", "low", "medium", "high"] }], - ["gemini-3.1-pro-preview", { thinkingLevels: ["low", "medium", "high"] }], - ["gemini-3.1-flash-lite-preview", { thinkingLevels: ["minimal", "low", "medium", "high"] }], - // gemini-3.5-flash is in the SDK's GoogleInteractionsModelId union. - // The plan originally excluded it citing v1beta2 routing, but the SDK - // targets /v1beta and includes it, so we allowlist it here. - ["gemini-3.5-flash", { thinkingLevels: ["minimal", "low", "medium", "high"] }], -]); - -/** - * Return the Interactions API profile for a Gemini model, or `undefined` if - * the model is not eligible. - * - * **Fail-closed**: unlisted model IDs return `undefined`. - */ -export function getGeminiInteractionsProfile( - modelId: string, -): GeminiInteractionsProfile | undefined { - return INTERACTIONS_PROFILES.get(modelId); -} - -/** - * Whether a model ID is eligible for the Interactions API. - */ -export function isInteractionsEligible(modelId: string): boolean { - return INTERACTIONS_PROFILES.has(modelId); -} diff --git a/packages/ai-provider-bridge/src/model-capabilities/gemma-helpers.ts b/packages/ai-config/src/model-capabilities/gemma-helpers.ts similarity index 96% rename from packages/ai-provider-bridge/src/model-capabilities/gemma-helpers.ts rename to packages/ai-config/src/model-capabilities/gemma-helpers.ts index 4a85c7b..f167f25 100644 --- a/packages/ai-provider-bridge/src/model-capabilities/gemma-helpers.ts +++ b/packages/ai-config/src/model-capabilities/gemma-helpers.ts @@ -2,7 +2,7 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import type { ModelInfo } from "../types"; +import type { InferredModelCapabilities as ModelInfo } from "../types.js"; // --------------------------------------------------------------------------- // Gemma model capability inference diff --git a/packages/ai-provider-bridge/src/model-capabilities/openai-helpers.ts b/packages/ai-config/src/model-capabilities/openai-helpers.ts similarity index 98% rename from packages/ai-provider-bridge/src/model-capabilities/openai-helpers.ts rename to packages/ai-config/src/model-capabilities/openai-helpers.ts index abd033a..dcf94db 100644 --- a/packages/ai-provider-bridge/src/model-capabilities/openai-helpers.ts +++ b/packages/ai-config/src/model-capabilities/openai-helpers.ts @@ -2,7 +2,7 @@ * Copyright (C) 2025-2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import type { ModelInfo } from "../types"; +import type { InferredModelCapabilities as ModelInfo } from "../types.js"; const OPENAI_THINKING_EFFORT_LEVELS = ["off", "low", "medium", "high"]; diff --git a/packages/ai-provider-bridge/src/model-capabilities/positai-helpers.ts b/packages/ai-config/src/model-capabilities/positai-helpers.ts similarity index 84% rename from packages/ai-provider-bridge/src/model-capabilities/positai-helpers.ts rename to packages/ai-config/src/model-capabilities/positai-helpers.ts index 54274ae..55bb74f 100644 --- a/packages/ai-provider-bridge/src/model-capabilities/positai-helpers.ts +++ b/packages/ai-config/src/model-capabilities/positai-helpers.ts @@ -2,9 +2,9 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import type { ModelInfo } from "../types"; -import { getAnthropicModelCapabilities } from "./anthropic-helpers"; -import { getGemmaModelCapabilities } from "./gemma-helpers"; +import type { InferredModelCapabilities as ModelInfo } from "../types.js"; +import { getAnthropicModelCapabilities } from "./anthropic-helpers.js"; +import { getGemmaModelCapabilities } from "./gemma-helpers.js"; /** * Infer model capabilities for a Posit AI model identifier. diff --git a/packages/ai-config/src/types.ts b/packages/ai-config/src/types.ts index f6405ec..0391156 100644 --- a/packages/ai-config/src/types.ts +++ b/packages/ai-config/src/types.ts @@ -182,8 +182,24 @@ export interface ModelInfoLike { supportedInputMediaTypes?: string[]; supportsWebSearch: boolean; thinkingEffortLevels?: string[]; + /** Whether the model requires vLLM-style `chat_template_kwargs` to enable thinking. */ + requiresChatTemplateKwargs?: boolean; } +/** + * The complete capability set the model-capability helpers can infer for a + * model known only by provider and id. Derived from {@link ModelInfoLike} so + * the package keeps a single ModelInfo mirror: identity/routing fields are + * dropped, and `protocol` is narrowed to the canonical {@link Protocol} union + * (set only where inference determines the wire protocol — Snowflake). + */ +export type InferredModelCapabilities = Omit< + ModelInfoLike, + "id" | "name" | "baseUrl" | "protocol" +> & { + protocol?: Protocol; +}; + /** * Output of `resolveModels()` — a model with resolved routing information. * Extends the input model with the protocol and endpoint resolved from the diff --git a/packages/ai-provider-bridge/src/__tests__/gemini-interactions.test.ts b/packages/ai-provider-bridge/src/__tests__/gemini-interactions.test.ts index 16dbc86..78f73dc 100644 --- a/packages/ai-provider-bridge/src/__tests__/gemini-interactions.test.ts +++ b/packages/ai-provider-bridge/src/__tests__/gemini-interactions.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it } from "vitest"; import { getGeminiInteractionsProfile, isInteractionsEligible, -} from "../model-capabilities/gemini-helpers"; +} from "../model-capabilities/gemini-interactions"; import { buildInteractionsOptions, extractPreviousInteractionId, diff --git a/packages/ai-provider-bridge/src/index.ts b/packages/ai-provider-bridge/src/index.ts index 17d11dd..44828ad 100644 --- a/packages/ai-provider-bridge/src/index.ts +++ b/packages/ai-provider-bridge/src/index.ts @@ -70,19 +70,20 @@ export type { AuthProviderMapping } from "./provider-map"; // the vscode-coupled `ai-provider-bridge/positron` module. export { CONFIG_KEY_OVERRIDES } from "./credential-shaping"; -// Model capability inference -export { getAnthropicModelCapabilities } from "./model-capabilities/anthropic-helpers"; +// Model capability inference (tables live in ai-config — ai-lib#9; the +// Gemini Interactions API allowlist is bridge routing logic and stays here) export { + getAnthropicModelCapabilities, getGeminiModelCapabilities, - getGeminiInteractionsProfile, - isInteractionsEligible, -} from "./model-capabilities/gemini-helpers"; -export type { GeminiInteractionsProfile } from "./model-capabilities/gemini-helpers"; -export { getOpenAIModelCapabilities, + getPositAiModelCapabilities, openaiMaxInputTokens, -} from "./model-capabilities/openai-helpers"; -export { getPositAiModelCapabilities } from "./model-capabilities/positai-helpers"; +} from "ai-config"; +export { + getGeminiInteractionsProfile, + isInteractionsEligible, +} from "./model-capabilities/gemini-interactions"; +export type { GeminiInteractionsProfile } from "./model-capabilities/gemini-interactions"; // Tool result image transformation for Chat Completions API compatibility export { diff --git a/packages/ai-provider-bridge/src/model-capabilities/gemini-interactions.ts b/packages/ai-provider-bridge/src/model-capabilities/gemini-interactions.ts new file mode 100644 index 0000000..8e77e7d --- /dev/null +++ b/packages/ai-provider-bridge/src/model-capabilities/gemini-interactions.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2026 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Gemini Interactions API eligibility (allowlist + thinking profiles). + * + * This is bridge routing logic — it decides which SDK surface GeminiClient + * speaks — so it stays here when the dependency-free capability tables move + * to ai-config (ai-lib#9). + */ + +/** + * Profile for a model eligible for the Gemini Interactions API. + */ +export interface GeminiInteractionsProfile { + /** Valid wire-level thinkingLevel values for this model. */ + thinkingLevels: readonly string[]; +} + +/** + * Explicit enumerated allowlist of model IDs reachable via + * `POST /v1beta/interactions`. **Fail-closed**: unlisted IDs are excluded. + * + * To add a model: add its exact ID here with a profile, then update the + * corresponding CAPABILITY_RULES entry and tests. + * + * thinkingLevels must match the Interactions API docs for each model. + * The builder validates against these — levels not listed here are + * clamped to "medium". + */ +const INTERACTIONS_PROFILES: ReadonlyMap = new Map([ + // --- Gemini 2.5 (thinkingLevel: low/medium/high) --- + ["gemini-2.5-pro", { thinkingLevels: ["low", "medium", "high"] }], + ["gemini-2.5-flash", { thinkingLevels: ["low", "medium", "high"] }], + ["gemini-2.5-flash-lite", { thinkingLevels: ["low", "medium", "high"] }], + + // --- Gemini 3.x --- + ["gemini-3-flash-preview", { thinkingLevels: ["minimal", "low", "medium", "high"] }], + ["gemini-3.1-pro-preview", { thinkingLevels: ["low", "medium", "high"] }], + ["gemini-3.1-flash-lite-preview", { thinkingLevels: ["minimal", "low", "medium", "high"] }], + // gemini-3.5-flash is in the SDK's GoogleInteractionsModelId union. + // The plan originally excluded it citing v1beta2 routing, but the SDK + // targets /v1beta and includes it, so we allowlist it here. + ["gemini-3.5-flash", { thinkingLevels: ["minimal", "low", "medium", "high"] }], +]); + +/** + * Return the Interactions API profile for a Gemini model, or `undefined` if + * the model is not eligible. + * + * **Fail-closed**: unlisted model IDs return `undefined`. + */ +export function getGeminiInteractionsProfile( + modelId: string, +): GeminiInteractionsProfile | undefined { + return INTERACTIONS_PROFILES.get(modelId); +} + +/** + * Whether a model ID is eligible for the Interactions API. + */ +export function isInteractionsEligible(modelId: string): boolean { + return INTERACTIONS_PROFILES.has(modelId); +} diff --git a/packages/ai-provider-bridge/src/model-clients/GeminiClient.ts b/packages/ai-provider-bridge/src/model-clients/GeminiClient.ts index 959108b..e772f41 100644 --- a/packages/ai-provider-bridge/src/model-clients/GeminiClient.ts +++ b/packages/ai-provider-bridge/src/model-clients/GeminiClient.ts @@ -18,7 +18,7 @@ import type { ModelMessage } from "ai"; import { streamText } from "ai"; import { safeSdkCustomHeaders } from "../custom-headers"; -import { getGeminiInteractionsProfile } from "../model-capabilities/gemini-helpers"; +import { getGeminiInteractionsProfile } from "../model-capabilities/gemini-interactions"; import type { AiToolWithJsonSchema, LMStreamPart, Logger } from "../types"; import { convertAiSdkStreamToPlatform, diff --git a/packages/ai-provider-bridge/src/positron/vscode-lm-models.ts b/packages/ai-provider-bridge/src/positron/vscode-lm-models.ts index 7d434f7..f55c395 100644 --- a/packages/ai-provider-bridge/src/positron/vscode-lm-models.ts +++ b/packages/ai-provider-bridge/src/positron/vscode-lm-models.ts @@ -9,14 +9,14 @@ * enrichment for vscode.lm models. */ -import * as vscode from "vscode"; - -import { getAnthropicModelCapabilities } from "../model-capabilities/anthropic-helpers"; -import { getGeminiModelCapabilities } from "../model-capabilities/gemini-helpers"; import { + getAnthropicModelCapabilities, + getGeminiModelCapabilities, getOpenAIModelCapabilities, openaiMaxInputTokens, -} from "../model-capabilities/openai-helpers"; +} from "ai-config"; +import * as vscode from "vscode"; + import { MAPPED_PROVIDER_IDS, PROVIDER_MAP } from "../provider-map"; import type { ModelInfo, ProviderId } from "../types"; import { PROVIDER_IDS } from "../types"; diff --git a/packages/ai-provider-bridge/src/providers/anthropic-provider.ts b/packages/ai-provider-bridge/src/providers/anthropic-provider.ts index a9dd625..e904bd5 100644 --- a/packages/ai-provider-bridge/src/providers/anthropic-provider.ts +++ b/packages/ai-provider-bridge/src/providers/anthropic-provider.ts @@ -2,7 +2,8 @@ * Copyright (C) 2025-2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { getAnthropicModelCapabilities } from "../model-capabilities/anthropic-helpers"; +import { getAnthropicModelCapabilities } from "ai-config"; + import { ANTHROPIC_API_VERSION, ANTHROPIC_HOST, diff --git a/packages/ai-provider-bridge/src/providers/bedrock-provider.ts b/packages/ai-provider-bridge/src/providers/bedrock-provider.ts index 6b14918..c18ee43 100644 --- a/packages/ai-provider-bridge/src/providers/bedrock-provider.ts +++ b/packages/ai-provider-bridge/src/providers/bedrock-provider.ts @@ -7,8 +7,8 @@ import { ListFoundationModelsCommand, } from "@aws-sdk/client-bedrock"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { getAnthropicModelCapabilities } from "ai-config"; -import { getAnthropicModelCapabilities } from "../model-capabilities/anthropic-helpers"; import { BedrockClient } from "../model-clients/BedrockClient"; import type { AwsCredentials, Logger, ModelInfo, ProviderCredentials } from "../types"; import { NOTIFICATION_ACTIONS } from "../types"; diff --git a/packages/ai-provider-bridge/src/providers/deepseek-provider.ts b/packages/ai-provider-bridge/src/providers/deepseek-provider.ts index acb53ee..6a129fe 100644 --- a/packages/ai-provider-bridge/src/providers/deepseek-provider.ts +++ b/packages/ai-provider-bridge/src/providers/deepseek-provider.ts @@ -2,7 +2,8 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { getDeepSeekModelCapabilities } from "../model-capabilities/deepseek-helpers"; +import { getDeepSeekModelCapabilities } from "ai-config"; + import { DeepSeekClient } from "../model-clients/DeepSeekClient"; import type { ApiKeyCredentials, Logger, ModelInfo } from "../types"; import { createCachedModelFetcher } from "./cached-model-fetcher"; diff --git a/packages/ai-provider-bridge/src/providers/gemini-provider.ts b/packages/ai-provider-bridge/src/providers/gemini-provider.ts index 9fb4f88..30548a0 100644 --- a/packages/ai-provider-bridge/src/providers/gemini-provider.ts +++ b/packages/ai-provider-bridge/src/providers/gemini-provider.ts @@ -2,10 +2,9 @@ * Copyright (C) 2025-2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { - getGeminiModelCapabilities, - isInteractionsEligible, -} from "../model-capabilities/gemini-helpers"; +import { getGeminiModelCapabilities } from "ai-config"; + +import { isInteractionsEligible } from "../model-capabilities/gemini-interactions"; import { GEMINI_API_VERSION, GEMINI_HOST, GeminiClient } from "../model-clients/GeminiClient"; import type { Logger, ModelInfo } from "../types"; import type { ApiKeyCredentials } from "../types"; diff --git a/packages/ai-provider-bridge/src/providers/google-vertex-provider.ts b/packages/ai-provider-bridge/src/providers/google-vertex-provider.ts index 65796a5..f3726d4 100644 --- a/packages/ai-provider-bridge/src/providers/google-vertex-provider.ts +++ b/packages/ai-provider-bridge/src/providers/google-vertex-provider.ts @@ -2,9 +2,9 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ +import { getAnthropicModelCapabilities } from "ai-config"; import { GoogleAuth } from "google-auth-library"; -import { getAnthropicModelCapabilities } from "../model-capabilities/anthropic-helpers"; import { GoogleVertexClient } from "../model-clients/GoogleVertexClient"; import type { Logger, ModelInfo, ProviderCredentials } from "../types"; import { NOTIFICATION_ACTIONS } from "../types"; diff --git a/packages/ai-provider-bridge/src/providers/openai-provider.ts b/packages/ai-provider-bridge/src/providers/openai-provider.ts index 55b0106..534113e 100644 --- a/packages/ai-provider-bridge/src/providers/openai-provider.ts +++ b/packages/ai-provider-bridge/src/providers/openai-provider.ts @@ -2,10 +2,8 @@ * Copyright (C) 2025-2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { - getOpenAIModelCapabilities, - openaiMaxInputTokens, -} from "../model-capabilities/openai-helpers"; +import { getOpenAIModelCapabilities, openaiMaxInputTokens } from "ai-config"; + import { OPENAI_API_VERSION, OPENAI_HOST, OpenAIClient } from "../model-clients/OpenAIClient"; import type { Logger, ModelInfo } from "../types"; import type { ApiKeyCredentials } from "../types"; diff --git a/packages/ai-provider-bridge/src/providers/openrouter-provider.ts b/packages/ai-provider-bridge/src/providers/openrouter-provider.ts index a6ff6bd..b1da0fc 100644 --- a/packages/ai-provider-bridge/src/providers/openrouter-provider.ts +++ b/packages/ai-provider-bridge/src/providers/openrouter-provider.ts @@ -2,7 +2,8 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { getAnthropicModelCapabilities } from "../model-capabilities/anthropic-helpers"; +import { getAnthropicModelCapabilities } from "ai-config"; + import { OpenRouterClient } from "../model-clients/OpenRouterClient"; import type { Logger, ModelInfo } from "../types"; import type { ApiKeyCredentials } from "../types"; diff --git a/packages/ai-provider-bridge/src/providers/positai-provider.ts b/packages/ai-provider-bridge/src/providers/positai-provider.ts index d66c008..c114315 100644 --- a/packages/ai-provider-bridge/src/providers/positai-provider.ts +++ b/packages/ai-provider-bridge/src/providers/positai-provider.ts @@ -2,7 +2,8 @@ * Copyright (C) 2025 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { getPositAiModelCapabilities } from "../model-capabilities/positai-helpers"; +import { getPositAiModelCapabilities } from "ai-config"; + import { PositAiClient } from "../model-clients/PositAiClient"; import type { Logger, diff --git a/packages/ai-provider-bridge/src/providers/snowflake-cortex-provider.ts b/packages/ai-provider-bridge/src/providers/snowflake-cortex-provider.ts index d117c8c..5030b0e 100644 --- a/packages/ai-provider-bridge/src/providers/snowflake-cortex-provider.ts +++ b/packages/ai-provider-bridge/src/providers/snowflake-cortex-provider.ts @@ -2,8 +2,8 @@ * Copyright (C) 2026 Posit Software, PBC. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { getAnthropicModelCapabilities } from "../model-capabilities/anthropic-helpers"; -import { getOpenAIModelCapabilities } from "../model-capabilities/openai-helpers"; +import { getAnthropicModelCapabilities, getOpenAIModelCapabilities } from "ai-config"; + import { SnowflakeClient } from "../model-clients/SnowflakeClient"; import type { Logger, ModelInfo, ProviderCredentials } from "../types"; import type { ProviderRegistry } from "./ProviderRegistry"; From c087212aa7289dcc1831b6096c80835924b66cf5 Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Wed, 15 Jul 2026 11:55:33 -0400 Subject: [PATCH 2/8] Add inferModelCapabilities to ai-config --- packages/ai-config/src/index.ts | 1 + .../__tests__/infer.test.ts | 84 ++++++++++++ .../ai-config/src/model-capabilities/infer.ts | 121 ++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 packages/ai-config/src/model-capabilities/__tests__/infer.test.ts create mode 100644 packages/ai-config/src/model-capabilities/infer.ts diff --git a/packages/ai-config/src/index.ts b/packages/ai-config/src/index.ts index 8b81760..08b5bf8 100644 --- a/packages/ai-config/src/index.ts +++ b/packages/ai-config/src/index.ts @@ -74,6 +74,7 @@ export { openaiMaxInputTokens, } from "./model-capabilities/openai-helpers.js"; export { getPositAiModelCapabilities } from "./model-capabilities/positai-helpers.js"; +export { inferModelCapabilities } from "./model-capabilities/infer.js"; // --- Defaults -------------------------------------------------------------- export { diff --git a/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts new file mode 100644 index 0000000..75f4766 --- /dev/null +++ b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2026 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; + +import { inferModelCapabilities } from "../infer.js"; + +describe("inferModelCapabilities", () => { + it("returns the generic baseline for an unknown model on an unknown provider", () => { + expect(inferModelCapabilities("openai-compatible", "totally-unknown-model")).toEqual({ + maxContextLength: 128_000, + supportsTools: true, + supportsImages: false, + supportsToolResultImages: false, + supportsWebSearch: false, + maxInputTokens: 128_000, + maxOutputTokens: 16_384, + }); + }); + + it("applies anthropic family inference above the baseline", () => { + const caps = inferModelCapabilities("anthropic", "claude-opus-4-8"); + expect(caps.maxContextLength).toBe(1_000_000); + expect(caps.maxOutputTokens).toBe(128_000); + expect(caps.maxInputTokens).toBe(1_000_000 - 128_000); + expect(caps.supportsTools).toBe(true); + expect(caps.supportsToolResultImages).toBe(true); + expect(caps.family).toBe("claude-4.8"); + }); + + it("uses the anthropic table for bedrock ids", () => { + const caps = inferModelCapabilities("bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"); + expect(caps.family).toBe("claude-4.5"); + expect(caps.maxContextLength).toBe(200_000); + expect(caps.maxOutputTokens).toBe(16_000); + expect(caps.maxInputTokens).toBe(200_000 - 16_000); + }); + + it("derives supportsImages when a table lists image media types but omits the flag", () => { + // The anthropic and gemini tables set supportedInputMediaTypes but never + // supportsImages; the derivation must lift the flag above the baseline. + expect(inferModelCapabilities("anthropic", "claude-opus-4-8").supportsImages).toBe(true); + expect(inferModelCapabilities("gemini", "gemini-2.5-pro").supportsImages).toBe(true); + }); + + it("derives openai maxInputTokens from the context window", () => { + const caps = inferModelCapabilities("openai", "gpt-4o"); + expect(caps.maxContextLength).toBe(128_000); + expect(caps.maxOutputTokens).toBe(16_384); + expect(caps.maxInputTokens).toBe(128_000 - 16_384); + }); + + it("maps the deepseek table, treating the input limit as the window", () => { + const caps = inferModelCapabilities("deepseek", "deepseek-chat"); + expect(caps.maxInputTokens).toBe(1_000_000); + expect(caps.maxContextLength).toBe(1_000_000); + expect(caps.supportsTools).toBe(true); + expect(caps.supportsImages).toBe(false); + }); + + it("resolves snowflake claude ids to the anthropic-messages protocol", () => { + expect(inferModelCapabilities("snowflake-cortex", "claude-sonnet-4-5").protocol).toBe( + "anthropic-messages", + ); + }); + + it("resolves snowflake non-claude ids to openai-chat and strips the openai- prefix", () => { + const caps = inferModelCapabilities("snowflake-cortex", "openai-gpt-4o"); + expect(caps.protocol).toBe("openai-chat"); + expect(caps.maxContextLength).toBe(128_000); // gpt-4o table matched after strip + expect(caps.family).toBe("gpt-4o"); + }); + + it("sets no protocol for non-snowflake providers", () => { + expect(inferModelCapabilities("anthropic", "claude-sonnet-4-5").protocol).toBeUndefined(); + }); + + it("passes gemma flags through the positai family", () => { + const caps = inferModelCapabilities("positai", "google/gemma-4-27b-it"); + expect(caps.requiresChatTemplateKwargs).toBe(true); + expect(caps.thinkingEffortLevels).toEqual(["off", "on"]); + }); +}); diff --git a/packages/ai-config/src/model-capabilities/infer.ts b/packages/ai-config/src/model-capabilities/infer.ts new file mode 100644 index 0000000..cc2f012 --- /dev/null +++ b/packages/ai-config/src/model-capabilities/infer.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2026 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { InferredModelCapabilities } from "../types.js"; +import { getAnthropicModelCapabilities } from "./anthropic-helpers.js"; +import { getDeepSeekModelCapabilities } from "./deepseek-helpers.js"; +import { getGeminiModelCapabilities } from "./gemini-helpers.js"; +import { getOpenAIModelCapabilities, openaiMaxInputTokens } from "./openai-helpers.js"; +import { getPositAiModelCapabilities } from "./positai-helpers.js"; + +/** + * Safe conservative baseline: enough that a text-only chat works with any + * provider that accepts the id. Mirrors the assistant's GENERIC_BASELINE + * (packages/positron/src/utils/model-override.ts), which this function + * replaces as the shared home of the inference chain. + */ +const GENERIC_BASELINE = { + supportsTools: true, + supportsImages: false, + supportsToolResultImages: false, + supportsWebSearch: false, + maxInputTokens: 128_000, + maxOutputTokens: 16_384, + maxContextLength: 128_000, +} as const; + +/** Provider-family inference: which capability table applies for this provider's ids. */ +function familyDefaults(providerId: string, modelId: string): Partial { + switch (providerId) { + case "anthropic": + case "bedrock": + return getAnthropicModelCapabilities(modelId) ?? {}; + case "openai": { + const caps = getOpenAIModelCapabilities(modelId); + if (!caps) return {}; + return { ...caps, maxInputTokens: openaiMaxInputTokens(caps) }; + } + case "positai": + return getPositAiModelCapabilities(modelId) ?? {}; + case "gemini": + return getGeminiModelCapabilities(modelId) ?? {}; + case "deepseek": { + const caps = getDeepSeekModelCapabilities(modelId); + return { + family: caps.family, + maxInputTokens: caps.maxInputTokens, + maxOutputTokens: caps.maxOutputTokens, + // DeepSeek publishes no separate context-window figure; the bridge's + // provider treats the input limit as the window (deepseek-provider.ts). + maxContextLength: caps.maxInputTokens, + supportsTools: caps.supportsTools, + supportsImages: caps.supportsImages, + thinkingEffortLevels: caps.thinkingEffortLevels, + }; + } + case "snowflake-cortex": { + // Claude on Snowflake speaks the Anthropic Messages API; everything + // else goes through Chat Completions. Snowflake ids may carry an + // `openai-` prefix the OpenAI lookup must not see. + const claude = getAnthropicModelCapabilities(modelId); + if (claude) { + return { ...claude, protocol: "anthropic-messages" }; + } + const openai = getOpenAIModelCapabilities(modelId.replace(/^openai-/, "")); + return { ...(openai ?? {}), protocol: "openai-chat" }; + } + default: + // ms-foundry, openai-compatible, custom provider ids: unknown + // endpoints, stay conservative. + return {}; + } +} + +/** + * Family inference plus a derivation the tables themselves omit: the + * anthropic/gemini tables list image input MIME types but never set + * `supportsImages`, so without this the baseline `false` would win for models + * that plainly accept images. Explicit table values (e.g. gpt-3.5's + * `supportsImages: false`) are never overridden. + */ +function inferProviderDefaults( + providerId: string, + modelId: string, +): Partial { + const caps = familyDefaults(providerId, modelId); + if ( + caps.supportsImages === undefined && + caps.supportedInputMediaTypes?.some((mediaType) => mediaType.startsWith("image/")) + ) { + return { ...caps, supportsImages: true }; + } + return caps; +} + +/** + * Infer a complete capability set for a model known only by provider and id: + * the generic baseline merged under provider-family inference (inference wins + * per field). Every required capability field is always present; optional + * fields (family, media types, thinking levels, protocol) appear only when + * inference determined them. + */ +export function inferModelCapabilities( + providerId: string, + modelId: string, +): InferredModelCapabilities { + const inferred = inferProviderDefaults(providerId, modelId); + // `??` per required field (not a bare spread) so a helper that explicitly + // sets a field to `undefined` cannot shadow the baseline. + return { + ...inferred, + maxContextLength: inferred.maxContextLength ?? GENERIC_BASELINE.maxContextLength, + maxInputTokens: inferred.maxInputTokens ?? GENERIC_BASELINE.maxInputTokens, + maxOutputTokens: inferred.maxOutputTokens ?? GENERIC_BASELINE.maxOutputTokens, + supportsTools: inferred.supportsTools ?? GENERIC_BASELINE.supportsTools, + supportsImages: inferred.supportsImages ?? GENERIC_BASELINE.supportsImages, + supportsToolResultImages: + inferred.supportsToolResultImages ?? GENERIC_BASELINE.supportsToolResultImages, + supportsWebSearch: inferred.supportsWebSearch ?? GENERIC_BASELINE.supportsWebSearch, + }; +} From ca95ba36e2681824decd9e3ab8c87fbccac104ec Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Wed, 15 Jul 2026 12:14:40 -0400 Subject: [PATCH 3/8] Document capability move and inferModelCapabilities --- CLAUDE.md | 19 +++--- memory-bank/aiConfig.md | 122 +++++++++++++++++++++++++++-------- memory-bank/architecture.md | 46 ++++++------- memory-bank/providerGuide.md | 24 +++---- packages/ai-config/README.md | 33 ++++++++-- 5 files changed, 167 insertions(+), 77 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 72f3490..3b22eb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,17 +8,17 @@ This file provides guidance to AI agents working in the `ai-lib` repository (Git The repo is consumed as a **git submodule** (`packages/ai-lib`) by the Posit Assistant monorepo, where all three packages are built from source as workspaces (resolved via `"": "*"`), not installed from published tarballs. -| Package | Purpose | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ai-provider-bridge` | LLM provider infra: plugin registry, model clients (14 providers), credential abstractions, and a Positron VS Code layer | -| `ai-config` | `~/.posit/ai/providers.json` schema, validation, defaults, and the load → enforce → build → watch resolution pipeline | -| `ai-credentials` | Credential resolution: browser-safe types/shaping, a generic single-file KV store, the store-backed backend + on-disk format, a vscode backend, and a root resolver (device-flow/refresh) | +| Package | Purpose | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ai-provider-bridge` | LLM provider infra: plugin registry, model clients (14 providers), credential abstractions, and a Positron VS Code layer | +| `ai-config` | `~/.posit/ai/providers.json` schema, validation, defaults, and the load → enforce → build → watch resolution pipeline; also owns shared model metadata: the capability tables and `inferModelCapabilities` (a deliberate charter expansion beyond providers.json handling — see below) | +| `ai-credentials` | Credential resolution: browser-safe types/shaping, a generic single-file KV store, the store-backed backend + on-disk format, a vscode backend, and a root resolver (device-flow/refresh) | ### Dependency relationships - `ai-provider-bridge` depends on `ai-config` and `ai-credentials` (`"ai-config": "*"`, `"ai-credentials": "*"`, resolve to workspaces) so both build first. - `ai-config` and `ai-credentials` are standalone leaves — neither depends on a sibling. -- `ai-config` and `ai-provider-bridge` share a vocabulary (provider IDs, protocols, client kinds), but with **no import edge** between them. A compile-time **shape guard** in `typechecks/` keeps them compatible (see [Shape Guard](#shape-guard-cross-package-vocabulary-compatibility)). +- `ai-config` must never import `ai-provider-bridge` (or any sibling); the bridge imports `ai-config` freely (types, capability helpers). The **vocabulary constants** (provider IDs, protocols, client kinds) remain duplicated on each side — never imported across — so a bridge type change cannot silently alter the disk format; the shape guard in `typechecks/` keeps them in sync (see [Shape Guard](#shape-guard-cross-package-vocabulary-compatibility)). ### Project Structure @@ -40,7 +40,7 @@ ai-lib/ ``` packages/ai-provider-bridge/src/ ├── model-clients/ # ModelClient implementations (one per provider) -├── model-capabilities/ # Per-provider capability inference helpers +├── model-capabilities/ # Gemini Interactions API allowlist (capability tables live in ai-config) ├── providers/ # register*Provider() modules + ProviderRegistry class ├── positron/ # VS Code integration (/positron entrypoint) ├── types.ts # PROVIDER_IDS, credential types, ProviderId @@ -68,7 +68,7 @@ packages/ai-provider-bridge/src/ | Entrypoint | What it provides | Node FS dep? | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| `ai-config` | Vocabulary, Zod schemas, inferred types, defaults, pure resolution helpers | No | +| `ai-config` | Vocabulary, Zod schemas, inferred types, defaults, pure resolution helpers, model capability tables, and `inferModelCapabilities` | No | | `ai-config/node` | The pure entry plus filesystem seams: `loadResolvedProviderCatalog`, `mutateProvidersConfig`, `watchResolvedProviderCatalog`, path constants | Yes | | `ai-config/providers.schema.json` | Generated JSON Schema for editor validation/autocomplete of `providers.json` | No | @@ -87,7 +87,7 @@ packages/ai-provider-bridge/src/ - `ai-provider-bridge` root entrypoint must NOT import `vscode` -- only `/positron` may. - No package may depend on a consumer (host) package -- the dependency arrow is one-way inward. - `ai-config` splits pure logic (`ai-config`) from filesystem I/O (`ai-config/node`); the pure entry must stay free of Node FS APIs. -- `ai-config` and `ai-provider-bridge` must not import each other; vocabulary compatibility is enforced by the shape guard. +- `ai-config` must never import `ai-provider-bridge` (or any sibling); the bridge imports `ai-config` freely (types, capability helpers). The vocabulary constants remain duplicated on each side — never imported across — so a bridge type change cannot silently alter the disk format; the shape guard in `typechecks/` keeps them in sync. - `ai-credentials`: `/types` stays browser-safe (no `vscode`/SDK/Node-builtins); `/store` imports no sibling; the root never imports `/store` (backends are injected); `/store-backend` never imports `@assistant/*`. `/store-backend` and `/positron` are the platform-bound (fs/vscode) entries. - `ai-credentials`'s `providerEnvMappings.ts` has a `-external` variant (empty map — positai has no secret env vars), redirected by the consuming app's build config. @@ -195,6 +195,7 @@ When adding a new provider: 4. Export from `src/providers.ts` 5. If it needs Positron auth, add to `PROVIDER_MAP` in `src/provider-map.ts` 6. Add the matching ID to `BUILTIN_PROVIDER_IDS` in `ai-config` so the shape guard passes +7. If the provider needs capability inference, add its table to `ai-config/src/model-capabilities/` and wire it into `inferModelCapabilities` (the tables live in `ai-config`, not the bridge) See `memory-bank/providerGuide.md` for the full step-by-step guide. diff --git a/memory-bank/aiConfig.md b/memory-bank/aiConfig.md index dd4e2a6..445fd51 100644 --- a/memory-bank/aiConfig.md +++ b/memory-bank/aiConfig.md @@ -27,11 +27,11 @@ enablement in Posit Assistant lives in the main monorepo's The package has three entrypoints, splitting pure (browser/test-safe) logic from filesystem I/O and vscode-bound wiring: -| Entrypoint | What it exports | External deps? | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | -| `ai-config` | Vocabulary, Zod schemas, inferred types, defaults, the pure resolution helpers (`resolveModels`, `mergeEnforced`), and the config-source contracts | No | -| `ai-config/node` | Re-exports the pure entry plus the three filesystem seams (`loadResolvedProviderCatalog`, `mutateProvidersConfig`, `watchResolvedProviderCatalog`) and path constants | Node FS | -| `ai-config/positron` | Builds a `host`-kind config source from Positron's `authentication.*` VS Code settings (injected via `additionalSources`); the pure `buildAuthenticationFragment` builder is testable without vscode | `vscode` | +| Entrypoint | What it exports | External deps? | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| `ai-config` | Vocabulary, Zod schemas, inferred types, defaults, the pure resolution helpers (`resolveModels`, `mergeEnforced`), the config-source contracts, and the model capability tables + `inferModelCapabilities` | No | +| `ai-config/node` | Re-exports the pure entry plus the three filesystem seams (`loadResolvedProviderCatalog`, `mutateProvidersConfig`, `watchResolvedProviderCatalog`) and path constants | Node FS | +| `ai-config/positron` | Builds a `host`-kind config source from Positron's `authentication.*` VS Code settings (injected via `additionalSources`); the pure `buildAuthenticationFragment` builder is testable without vscode | `vscode` | Each provider's `PositronAuthSettingDescriptor` (consumed by `buildAuthenticationFragment`) may carry an optional `normalizeBaseUrl?: (url: string) => string` hook, applied to the raw `baseUrl` setting before it enters the fragment. It's the seam for correcting known-bad values (e.g. a bare API host missing its version segment) without `ai-config` importing `ai-provider-bridge` — the consumer (`packages/positron`) injects the bridge's `normalizeBaseUrlForProvider` when building descriptors; `ai-config` only ever sees an opaque string-to-string function. | `ai-config/providers.schema.json` | The generated JSON Schema, exported so editors can validate/autocomplete `providers.json` | No | @@ -176,6 +176,72 @@ without managing locking, atomicity, or watch lifecycle themselves. injection (`$schema`, `version`) on first creation, and a best-effort copy of `providers.schema.json` alongside the config for editor validation. +## Model Capability Inference (`src/model-capabilities/`) + +`ai-config` also owns the shared model-metadata charter: per-provider capability +tables and `inferModelCapabilities(providerId, modelId)`, the single function +that turns a bare provider + model id into a complete capability set. This +moved here from `ai-provider-bridge` (ai-lib#9) so any `ai-config` consumer — +Positron's authentication extension, the assistant, future core — can +synthesize model capabilities without taking the bridge's dependency tree +(SDKs, `vscode` peer, etc.). The bridge re-exports the helpers it used to own +(`getAnthropicModelCapabilities`, `getGeminiModelCapabilities`, +`getOpenAIModelCapabilities`, `openaiMaxInputTokens`, +`getPositAiModelCapabilities`) from `ai-config` so none of its existing +consumers broke; its own `getGeminiInteractionsProfile` / +`isInteractionsEligible` stayed behind as `gemini-interactions.ts`, since that +allowlist is bridge SDK-routing logic (which wire API `GeminiClient` speaks), +not a dependency-free capability table. + +**The tables** (`anthropic-helpers.ts`, `deepseek-helpers.ts`, +`gemini-helpers.ts`, `gemma-helpers.ts`, `openai-helpers.ts`) are pure +regex-driven lookups from a provider-specific model id to a partial capability +set — no imports beyond `InferredModelCapabilities` (a projection of +`ModelInfoLike` with identity/routing fields dropped and `protocol` narrowed to +the canonical `Protocol` union). `positai-helpers.ts` composes the Anthropic +and Gemma tables, since Posit AI routes both families. + +**`inferModelCapabilities(providerId, modelId)`** (`src/model-capabilities/infer.ts`) +merges a conservative `GENERIC_BASELINE` (128k context, tools on, no images, +no web search) under provider-family inference, with inference winning per +field: + +- `anthropic` / `bedrock` → the Anthropic table (Bedrock ids carry the same + `claude-*` family, just prefixed). +- `openai` → the OpenAI table, with `maxInputTokens` re-derived via + `openaiMaxInputTokens()` (context window minus reserved output budget) — + the table itself doesn't set it. +- `positai` → the combined Anthropic/Gemma lookup. +- `gemini` → the Gemini table. +- `deepseek` → the DeepSeek table, mapped specially: DeepSeek publishes no + separate context-window figure, so `maxContextLength` is set equal to the + table's `maxInputTokens` (mirroring how `deepseek-provider.ts` in the bridge + already treats the input limit as the window). +- `snowflake-cortex` → tries the Anthropic table first (Claude on Snowflake + speaks the Anthropic Messages API); if that misses, strips a leading + `openai-` prefix and tries the OpenAI table. Either branch sets `protocol` + (`"anthropic-messages"` or `"openai-chat"`) — the only case where inference + determines the wire protocol. Every other provider leaves `protocol` + `undefined`. +- Anything else (`ms-foundry`, `openai-compatible`, custom provider ids) stays + at the generic baseline — those are unknown endpoints. + +One derivation sits above the per-family lookup: the Anthropic and Gemini +tables list `supportedInputMediaTypes` (image MIME types) but never set +`supportsImages` directly, so `inferProviderDefaults()` lifts `supportsImages` +to `true` whenever a table leaves it unset AND lists an `image/*` media type — +otherwise the baseline `false` would win for models that plainly accept +images. A table's explicit value (e.g. GPT-3.5's `supportsImages: false`) is +never overridden. + +`inferModelCapabilities` is the intended delegation target for +posit-dev/positron#14708 Task 8 and for the assistant's +`model-override.ts` (its `GENERIC_BASELINE` + inference chain is mirrored +here, with three improvements over that copy: the `openai` case now derives +`maxInputTokens` instead of leaving the 128k baseline, a `deepseek` case uses +the DeepSeek table, and `supportsImages` is derived from media types as +described above). + ## Shape Guard `typechecks/shape-guard.typecheck.ts` holds compile-time assertions (type-checked @@ -198,28 +264,30 @@ the bridge's `ModelInfo` — compatible by contract, not by import. ## Code Layout -| Location | What it does | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `src/vocabulary.ts` | Provider-ID / protocol / client-kind / reserved-key value tuples + type guards | -| `src/schema.ts` | Zod schemas (full + enforced variants) for `providers.json` | -| `src/types.ts` | Types inferred from Zod + resolution outputs + branded `CustomProviderId` / `mintCustomProviderId` | -| `src/defaults.ts` | Built-in provider connection defaults; `PROVIDER_CONNECTION_DEFAULTS` | -| `src/enforce.ts` | `mergeEnforced()` deep-merge of enforced over user config | -| `src/resolve-enabled.ts` | `resolveEnabled()` enablement precedence ladder | -| `src/resolve-connection.ts` | Internal baseUrl/endpoint resolution precedence | -| `src/resolve-models.ts` | `resolveModels()` model selection + routing pipeline | -| `src/index.ts` | Pure entrypoint exports | -| `src/node/paths.ts` | `AI_CONFIG_DIR`, `PROVIDERS_CONFIG_PATH`, enforced env-var name, lockfile path | -| `src/node/types.ts` | Node seam option/result types (`LoadCatalogOptions`, `ProviderCatalogChange`, `Disposable`, …) | -| `src/resolve-catalog.ts` | `resolveProviderCatalog()` — pure deep resolver seam; owns the precedence stack + sealed-enforced invariant | -| `src/build-catalog.ts` | `buildCatalog()` — assemble `ResolvedProvider[]` from the resolved config + baseline + env overlay (pure entry) | -| `src/node/load-config.ts` | `loadConfigSources()` / `readFileConfig()` / `readEnvFragment()` — assemble the ordered `ProviderConfigSource` list | -| `src/node/load-catalog.ts` | `loadResolvedProviderCatalog()` — public read seam (assemble sources → `resolveProviderCatalog`) | -| `src/node/mutate-config.ts` | `mutateProvidersConfig()` — locked, atomic, serialized mutation | -| `src/node/watch-catalog.ts` | `watchResolvedProviderCatalog()` — watch, reload, diff, emit typed changes | -| `src/node/index.ts` | Node entrypoint; re-exports pure entry + filesystem seams | -| `providers.schema.json` | Generated JSON Schema, exported for editor validation | -| `scripts/generate-schema.ts` | Regenerates `providers.schema.json` from the Zod schemas | +| Location | What it does | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `src/vocabulary.ts` | Provider-ID / protocol / client-kind / reserved-key value tuples + type guards | +| `src/schema.ts` | Zod schemas (full + enforced variants) for `providers.json` | +| `src/types.ts` | Types inferred from Zod + resolution outputs + branded `CustomProviderId` / `mintCustomProviderId` | +| `src/defaults.ts` | Built-in provider connection defaults; `PROVIDER_CONNECTION_DEFAULTS` | +| `src/enforce.ts` | `mergeEnforced()` deep-merge of enforced over user config | +| `src/resolve-enabled.ts` | `resolveEnabled()` enablement precedence ladder | +| `src/resolve-connection.ts` | Internal baseUrl/endpoint resolution precedence | +| `src/resolve-models.ts` | `resolveModels()` model selection + routing pipeline | +| `src/model-capabilities/*-helpers.ts` | Per-provider capability tables (moved from the bridge, ai-lib#9) | +| `src/model-capabilities/infer.ts` | `inferModelCapabilities()` — baseline + provider-family merge, Snowflake protocol rule | +| `src/index.ts` | Pure entrypoint exports | +| `src/node/paths.ts` | `AI_CONFIG_DIR`, `PROVIDERS_CONFIG_PATH`, enforced env-var name, lockfile path | +| `src/node/types.ts` | Node seam option/result types (`LoadCatalogOptions`, `ProviderCatalogChange`, `Disposable`, …) | +| `src/resolve-catalog.ts` | `resolveProviderCatalog()` — pure deep resolver seam; owns the precedence stack + sealed-enforced invariant | +| `src/build-catalog.ts` | `buildCatalog()` — assemble `ResolvedProvider[]` from the resolved config + baseline + env overlay (pure entry) | +| `src/node/load-config.ts` | `loadConfigSources()` / `readFileConfig()` / `readEnvFragment()` — assemble the ordered `ProviderConfigSource` list | +| `src/node/load-catalog.ts` | `loadResolvedProviderCatalog()` — public read seam (assemble sources → `resolveProviderCatalog`) | +| `src/node/mutate-config.ts` | `mutateProvidersConfig()` — locked, atomic, serialized mutation | +| `src/node/watch-catalog.ts` | `watchResolvedProviderCatalog()` — watch, reload, diff, emit typed changes | +| `src/node/index.ts` | Node entrypoint; re-exports pure entry + filesystem seams | +| `providers.schema.json` | Generated JSON Schema, exported for editor validation | +| `scripts/generate-schema.ts` | Regenerates `providers.schema.json` from the Zod schemas | ## Invariants & Design Decisions diff --git a/memory-bank/architecture.md b/memory-bank/architecture.md index 1193368..dc2dcce 100644 --- a/memory-bank/architecture.md +++ b/memory-bank/architecture.md @@ -12,12 +12,12 @@ package: ai-provider-bridge ## Entrypoints -| Entrypoint | What it exports | vscode dependency? | -| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ | -| `ai-provider-bridge` | `ProviderRegistry`, `ModelClient` interface, `StepLogger` interface, `CredentialProvider` interface, `createCachedModelFetcher`, `PROVIDER_MAP`, `MAPPED_PROVIDER_IDS` | No | -| `ai-provider-bridge/providers` | `register*Provider()` functions (14), all model client classes, AI SDK helpers, `openai-compat-fetch`, provider test utilities | No | -| `ai-provider-bridge/positron` | `VscodeLmClient`, `listVscodeLmModels()`, `fromAiMessages2()`, LM helpers, `isProviderId()`, `toProviderId()`, `CONFIG_KEY_OVERRIDES` (**no** `PositronCredentialProvider` — removed in Phase 7; the VS Code auth backend is `createPositronBackend` from `ai-credentials/positron`) | **Yes** | -| `ai-provider-bridge/credential-shaping` | `shapeCredentials()`, `CredentialConfig`, `CONFIG_KEY_OVERRIDES` -- pure credential shaping, browser-safe (no vscode, AI SDK, or node builtins); consumed by Positron's renderer facade | No | +| Entrypoint | What it exports | vscode dependency? | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `ai-provider-bridge` | `ProviderRegistry`, `ModelClient` interface, `StepLogger` interface, `CredentialProvider` interface, `createCachedModelFetcher`, `PROVIDER_MAP`, `MAPPED_PROVIDER_IDS`; re-exports the model capability helpers (`getAnthropicModelCapabilities`, `getGeminiModelCapabilities`, `getOpenAIModelCapabilities`, `openaiMaxInputTokens`, `getPositAiModelCapabilities`) from `ai-config`, plus its own `getGeminiInteractionsProfile`/`isInteractionsEligible` | No | +| `ai-provider-bridge/providers` | `register*Provider()` functions (14), all model client classes, AI SDK helpers, `openai-compat-fetch`, provider test utilities | No | +| `ai-provider-bridge/positron` | `VscodeLmClient`, `listVscodeLmModels()`, `fromAiMessages2()`, LM helpers, `isProviderId()`, `toProviderId()`, `CONFIG_KEY_OVERRIDES` (**no** `PositronCredentialProvider` — removed in Phase 7; the VS Code auth backend is `createPositronBackend` from `ai-credentials/positron`) | **Yes** | +| `ai-provider-bridge/credential-shaping` | `shapeCredentials()`, `CredentialConfig`, `CONFIG_KEY_OVERRIDES` -- pure credential shaping, browser-safe (no vscode, AI SDK, or node builtins); consumed by Positron's renderer facade | No | ## Invariants @@ -28,22 +28,22 @@ package: ai-provider-bridge ## Code Layout -| Location | What it does | VS Code deps? | -| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `src/types.ts` | `PROVIDER_IDS` tuple (14 internal-build IDs) and `ProviderId` type -- single source of truth for valid provider IDs | No | -| `src/providers/` | Provider registry, model fetchers, client factories (14 internal-build providers) | No | -| `src/model-clients/` | Chat API clients (Anthropic, OpenAI, Gemini, Bedrock, Snowflake, Copilot SDK, DeepSeek, etc.) via AI SDK | No | -| `src/model-capabilities/` | Per-provider capability inference helpers (model ID to capabilities mapping) | No | -| `src/provider-map.ts` | `PROVIDER_MAP` and `MAPPED_PROVIDER_IDS` -- maps logical provider IDs to Positron auth provider config | No | -| `src/credential-shaping.ts` | `shapeCredentials()` -- pure token-to-`ProviderCredentials` shaping over an injected `CredentialConfig` | No | -| `src/custom-headers.ts` | Header merging/filtering utilities for custom HTTP headers | No | -| `ai-credentials/src/positron/PositronBackend.ts` | `createPositronBackend` -- VS Code auth backend (in `ai-credentials`, not the bridge; replaces the removed `PositronCredentialProvider`) | **Yes** | -| `src/positron/VscodeLmClient.ts` | `VscodeLmClient` -- `ModelClient` implementation wrapping `vscode.LanguageModelChat` | **Yes** | -| `src/positron/vscode-lm-models.ts` | `listVscodeLmModels()`, `toProviderId()`, `isProviderId()`, vendor-to-provider mapping | **Yes** | -| `src/positron/message-formats.ts` | `fromAiMessages2()` (AI SDK to VS Code direction), cache control helpers | **Yes** | -| `src/positron/lm-helpers.ts` | Type guards and cache breakpoint helpers for VS Code LM parts | **Yes** | -| `src/positron/utils.ts` | `ensureUint8Array()` -- binary data normalization for cross-process data | No | -| `src/local-providers.ts` | `LocalProviderManager` class, `LOCAL_PROVIDER_IDS`, DI-based endpoint management (no vscode/node deps) | No | +| Location | What it does | VS Code deps? | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `src/types.ts` | `PROVIDER_IDS` tuple (14 internal-build IDs) and `ProviderId` type -- single source of truth for valid provider IDs | No | +| `src/providers/` | Provider registry, model fetchers, client factories (14 internal-build providers) | No | +| `src/model-clients/` | Chat API clients (Anthropic, OpenAI, Gemini, Bedrock, Snowflake, Copilot SDK, DeepSeek, etc.) via AI SDK | No | +| `src/model-capabilities/gemini-interactions.ts` | Gemini Interactions API allowlist (`getGeminiInteractionsProfile`, `isInteractionsEligible`) -- bridge SDK-routing logic; the per-provider capability tables themselves live in `ai-config` (moved in ai-lib#9) and are re-exported from the bridge root for existing consumers | No | +| `src/provider-map.ts` | `PROVIDER_MAP` and `MAPPED_PROVIDER_IDS` -- maps logical provider IDs to Positron auth provider config | No | +| `src/credential-shaping.ts` | `shapeCredentials()` -- pure token-to-`ProviderCredentials` shaping over an injected `CredentialConfig` | No | +| `src/custom-headers.ts` | Header merging/filtering utilities for custom HTTP headers | No | +| `ai-credentials/src/positron/PositronBackend.ts` | `createPositronBackend` -- VS Code auth backend (in `ai-credentials`, not the bridge; replaces the removed `PositronCredentialProvider`) | **Yes** | +| `src/positron/VscodeLmClient.ts` | `VscodeLmClient` -- `ModelClient` implementation wrapping `vscode.LanguageModelChat` | **Yes** | +| `src/positron/vscode-lm-models.ts` | `listVscodeLmModels()`, `toProviderId()`, `isProviderId()`, vendor-to-provider mapping | **Yes** | +| `src/positron/message-formats.ts` | `fromAiMessages2()` (AI SDK to VS Code direction), cache control helpers | **Yes** | +| `src/positron/lm-helpers.ts` | Type guards and cache breakpoint helpers for VS Code LM parts | **Yes** | +| `src/positron/utils.ts` | `ensureUint8Array()` -- binary data normalization for cross-process data | No | +| `src/local-providers.ts` | `LocalProviderManager` class, `LOCAL_PROVIDER_IDS`, DI-based endpoint management (no vscode/node deps) | No | ## Provider Inventory @@ -177,7 +177,7 @@ The bridge owns its provider SDKs: all of them are regular `dependencies`, so a ## Guidance for New Code -- **New provider modules, model clients, and capability helpers** go in `src/providers/`, `src/model-clients/`, and `src/model-capabilities/` +- **New provider modules and model clients** go in `src/providers/` and `src/model-clients/`; **new capability helpers** go in `ai-config`'s `src/model-capabilities/` (see `memory-bank/aiConfig.md`), not the bridge's - **New provider IDs** are added to `PROVIDER_IDS` in `src/types.ts` - **Positron auth mappings** are added to `PROVIDER_MAP` in `src/provider-map.ts`; the VS Code auth backend that consumes them is `createPositronBackend` in `ai-credentials/positron` - **Avoid adding new `vscode` dependencies** to the root entrypoint -- the `/positron` entrypoint is the correct place for VS Code integration diff --git a/memory-bank/providerGuide.md b/memory-bank/providerGuide.md index e4b9e57..c052427 100644 --- a/memory-bank/providerGuide.md +++ b/memory-bank/providerGuide.md @@ -52,11 +52,11 @@ Implement `ModelClient` interface with `chat()` method that returns `AsyncIterab ## Step 3b: Add Capability Helpers (Optional) -**File**: `src/model-capabilities/newprovider-helpers.ts` +**File**: `packages/ai-config/src/model-capabilities/newprovider-helpers.ts` -If the provider has model-specific capabilities (vision, thinking, embeddings), add a helper that maps model IDs to `ModelCapabilities`. This is used by the model fetcher to annotate each model. +If the provider has model-specific capabilities (vision, thinking, embeddings), add a helper that maps model IDs to `ModelCapabilities`. This is used by the model fetcher to annotate each model. The capability tables live in `ai-config` (moved there in ai-lib#9 so any `ai-config` consumer can call `inferModelCapabilities` without the bridge's dependency tree); the bridge imports the helper from `ai-config` at its call sites. -**Reference**: See `deepseek-helpers.ts`, `gemini-helpers.ts`, or `anthropic-helpers.ts`. +**Reference**: See `deepseek-helpers.ts`, `gemini-helpers.ts`, or `anthropic-helpers.ts` in `packages/ai-config/src/model-capabilities/`. ## Step 4: Implement Model Fetcher @@ -99,15 +99,15 @@ If it is a local endpoint provider, add it to `LOCAL_PROVIDER_IDS` in `src/local ## Files Summary -| File | Change | -| --------------------------------------- | ----------------------------------------- | -| `src/types.ts` | Add to `PROVIDER_IDS` | -| `src/model-clients/XyzClient.ts` | New client class | -| `src/model-capabilities/xyz-helpers.ts` | Optional: capability inference helpers | -| `src/providers/xyz-provider.ts` | New provider module | -| `src/providers.ts` | Export new registration function + client | -| `src/provider-map.ts` | Optional: add Positron auth mapping | -| `src/positron/auth.ts` | Optional: add credential handling | +| File | Change | +| ---------------------------------------------------- | ------------------------------------------------------------- | +| `src/types.ts` | Add to `PROVIDER_IDS` | +| `src/model-clients/XyzClient.ts` | New client class | +| `../ai-config/src/model-capabilities/xyz-helpers.ts` | Optional: capability inference helpers (lives in `ai-config`) | +| `src/providers/xyz-provider.ts` | New provider module | +| `src/providers.ts` | Export new registration function + client | +| `src/provider-map.ts` | Optional: add Positron auth mapping | +| `src/positron/auth.ts` | Optional: add credential handling | ## Thinking/Reasoning Support diff --git a/packages/ai-config/README.md b/packages/ai-config/README.md index 205ebeb..0e7636b 100644 --- a/packages/ai-config/README.md +++ b/packages/ai-config/README.md @@ -8,12 +8,12 @@ This package is part of the [`ai-lib`](../../README.md) monorepo. It is a depend The package splits pure (browser/test-safe) logic from filesystem I/O: -| Entrypoint | What it provides | Node FS dep? | -| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| `ai-config` | Vocabulary (`BUILTIN_PROVIDER_IDS`, `PROTOCOL_VALUES`, `CLIENT_KIND_VALUES`, …), Zod schemas, inferred types, defaults, the `resolveProviderCatalog({ sources })` seam, and pure helpers | No | -| `ai-config/node` | The pure entry plus the three filesystem seams and path constants | Yes | -| `ai-config/positron` | Builds a `host`-kind `ProviderConfigSource` from Positron `authentication.*` settings (+ change signal). The only entry that imports `vscode`. | No (vscode) | -| `ai-config/providers.schema.json` | The generated JSON Schema, for editor validation/autocomplete of `providers.json` | No | +| Entrypoint | What it provides | Node FS dep? | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | +| `ai-config` | Vocabulary (`BUILTIN_PROVIDER_IDS`, `PROTOCOL_VALUES`, `CLIENT_KIND_VALUES`, …), Zod schemas, inferred types, defaults, the `resolveProviderCatalog({ sources })` seam, pure helpers, and the model capability tables + `inferModelCapabilities` | No | +| `ai-config/node` | The pure entry plus the three filesystem seams and path constants | Yes | +| `ai-config/positron` | Builds a `host`-kind `ProviderConfigSource` from Positron `authentication.*` settings (+ change signal). The only entry that imports `vscode`. | No (vscode) | +| `ai-config/providers.schema.json` | The generated JSON Schema, for editor validation/autocomplete of `providers.json` | No | The three filesystem seams (`ai-config/node`): @@ -138,6 +138,27 @@ import { Built-in default connection config (base URLs, endpoints) applied as the lowest precedence layer beneath the file and env-var overlays. +#### Model capability inference + +```ts +import { + getAnthropicModelCapabilities, + getDeepSeekModelCapabilities, + getGeminiModelCapabilities, + getGemmaModelCapabilities, + getOpenAIModelCapabilities, + openaiMaxInputTokens, + getPositAiModelCapabilities, + inferModelCapabilities, +} from "ai-config"; + +import type { InferredModelCapabilities } from "ai-config"; +``` + +The per-provider `get*ModelCapabilities(modelId)` helpers are pure, regex-driven lookups that map a provider-specific model id to a `Partial` (or `undefined`/a default object when the id doesn't match that provider's family). `openaiMaxInputTokens(caps)` derives the OpenAI input-token ceiling from a capability object's context window and output-token reservation. + +`inferModelCapabilities(providerId, modelId): InferredModelCapabilities` is the single entry point that ties the tables together: it merges a conservative generic baseline (128k context, tools on, no images, no web search) under whichever provider-family table applies, with the table's values winning per field. It also derives `supportsImages` from a table's `supportedInputMediaTypes` when the table sets media types but leaves the flag itself unset, and resolves `protocol` for `snowflake-cortex` ids (Claude ids → `"anthropic-messages"`, everything else → `"openai-chat"` after stripping a leading `openai-` prefix). This is the function `ai-provider-bridge` re-exports at its root, and the intended delegation target for any consumer that needs model capabilities without the bridge's dependency tree. + --- ### Node entry (`ai-config/node`) From 995fd5f4f7adcddde24b101cf0f755829c1eab8e Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Wed, 15 Jul 2026 12:14:50 -0400 Subject: [PATCH 4/8] Add tsx as an ai-config devDependency --- package-lock.json | 1 + packages/ai-config/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 38a273b..84da3ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6185,6 +6185,7 @@ "@types/node": "22.x", "@types/proper-lockfile": "^4.1.4", "@types/vscode": "^1.100.0", + "tsx": "^4.20.6", "typescript": "^7.0.2", "vitest": "^3.2.1" }, diff --git a/packages/ai-config/package.json b/packages/ai-config/package.json index ee6e315..405718d 100644 --- a/packages/ai-config/package.json +++ b/packages/ai-config/package.json @@ -45,6 +45,7 @@ "@types/node": "22.x", "@types/proper-lockfile": "^4.1.4", "@types/vscode": "^1.100.0", + "tsx": "^4.20.6", "typescript": "^7.0.2", "vitest": "^3.2.1" }, From 8e9e0fd1be0a7196dca747ff970ac37477a3819f Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Wed, 15 Jul 2026 14:12:56 -0400 Subject: [PATCH 5/8] Fix snowflake/vertex capability inference and helper param type --- .../__tests__/infer.test.ts | 39 ++++++++++++ .../ai-config/src/model-capabilities/infer.ts | 63 +++++++++++++++---- .../src/model-capabilities/openai-helpers.ts | 14 ++++- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts index 75f4766..1ad48de 100644 --- a/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts +++ b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts @@ -81,4 +81,43 @@ describe("inferModelCapabilities", () => { expect(caps.requiresChatTemplateKwargs).toBe(true); expect(caps.thinkingEffortLevels).toEqual(["off", "on"]); }); + + it("caps snowflake claude ids to Snowflake's limits, not the upstream table", () => { + // The Anthropic table gives Opus 4.7 a 1M window / 128k output; Snowflake + // Cortex serves it at 200k / 16k (snowflake-cortex-provider.ts). + const caps = inferModelCapabilities("snowflake-cortex", "claude-opus-4-7"); + expect(caps.protocol).toBe("anthropic-messages"); + expect(caps.maxContextLength).toBe(200_000); + expect(caps.maxInputTokens).toBe(200_000); + expect(caps.maxOutputTokens).toBe(16_384); + expect(caps.supportsToolResultImages).toBe(true); + expect(caps.family).toBe("claude-4.7"); // still borrowed from the table + }); + + it("caps snowflake openai ids and disables tool-result images", () => { + const caps = inferModelCapabilities("snowflake-cortex", "openai-gpt-5.2"); + expect(caps.protocol).toBe("openai-chat"); + expect(caps.maxContextLength).toBe(128_000); + expect(caps.maxInputTokens).toBe(128_000); + expect(caps.maxOutputTokens).toBe(16_384); + expect(caps.supportsImages).toBe(true); // gpt-5.x accepts images + expect(caps.supportsToolResultImages).toBe(false); + }); + + it("infers google-vertex gemini models from the gemini table", () => { + const caps = inferModelCapabilities("google-vertex", "gemini-2.5-pro"); + expect(caps.maxContextLength).toBe(1_000_000); + expect(caps.supportsImages).toBe(true); + expect(caps.family).toBe("gemini-2.5"); + }); + + it("infers google-vertex anthropic partner models, stripping resource prefixes", () => { + const caps = inferModelCapabilities( + "google-vertex", + "publishers/anthropic/models/claude-opus-4-7", + ); + expect(caps.family).toBe("claude-4.7"); + expect(caps.maxContextLength).toBe(1_000_000); + expect(caps.supportsImages).toBe(true); + }); }); diff --git a/packages/ai-config/src/model-capabilities/infer.ts b/packages/ai-config/src/model-capabilities/infer.ts index cc2f012..3cbe7a6 100644 --- a/packages/ai-config/src/model-capabilities/infer.ts +++ b/packages/ai-config/src/model-capabilities/infer.ts @@ -25,6 +25,49 @@ const GENERIC_BASELINE = { maxContextLength: 128_000, } as const; +const SNOWFLAKE_IMAGE_MEDIA_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]; + +/** + * Snowflake Cortex serves a fixed catalog with its own caps, not the upstream + * model limits: Claude routes through the Anthropic Messages API (200k context + * / 16k output), everything else through Chat Completions (128k / 16k). Only + * `family` and thinking levels are borrowed from the upstream tables; token + * windows and feature flags follow snowflake-cortex-provider.ts. Snowflake + * OpenAI ids may carry an `openai-` prefix the OpenAI lookup must not see. + */ +function snowflakeDefaults(modelId: string): Partial { + const claude = getAnthropicModelCapabilities(modelId); + if (claude) { + return { + family: claude.family, + thinkingEffortLevels: claude.thinkingEffortLevels, + protocol: "anthropic-messages", + maxContextLength: 200_000, + maxInputTokens: 200_000, + maxOutputTokens: 16_384, + supportsTools: true, + supportsImages: true, + supportsToolResultImages: true, + supportedInputMediaTypes: SNOWFLAKE_IMAGE_MEDIA_TYPES, + }; + } + const openai = getOpenAIModelCapabilities(modelId.replace(/^openai-/, "")); + const supportsImages = + openai?.supportedInputMediaTypes?.some((mediaType) => mediaType.startsWith("image/")) ?? false; + return { + family: openai?.family, + thinkingEffortLevels: openai?.thinkingEffortLevels, + protocol: "openai-chat", + maxContextLength: 128_000, + maxInputTokens: 128_000, + maxOutputTokens: 16_384, + supportsTools: true, + supportsImages, + supportsToolResultImages: false, + supportedInputMediaTypes: supportsImages ? SNOWFLAKE_IMAGE_MEDIA_TYPES : undefined, + }; +} + /** Provider-family inference: which capability table applies for this provider's ids. */ function familyDefaults(providerId: string, modelId: string): Partial { switch (providerId) { @@ -40,6 +83,13 @@ function familyDefaults(providerId: string, modelId: string): Partial return undefined; } -/** OpenAI models share a context window; reserve space for output tokens. */ -export function openaiMaxInputTokens(model: Partial): number | undefined { +/** + * OpenAI models share a context window; reserve space for output tokens. + * + * Accepts a minimal structural type — only the two token fields are read — so + * callers passing any capability shape (bridge `Partial`, + * `Partial`, …) stay assignable regardless of how + * each type narrows unrelated fields such as `protocol`. + */ +export function openaiMaxInputTokens(model: { + maxContextLength?: number; + maxOutputTokens?: number; +}): number | undefined { if (model.maxContextLength === undefined || model.maxOutputTokens === undefined) { return undefined; } From 07ee78a3f57bf949773aafc1dd7d74f2baba04db Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Wed, 15 Jul 2026 14:55:15 -0400 Subject: [PATCH 6/8] Export customModelSchema from ai-config --- packages/ai-config/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ai-config/src/index.ts b/packages/ai-config/src/index.ts index 08b5bf8..c637db9 100644 --- a/packages/ai-config/src/index.ts +++ b/packages/ai-config/src/index.ts @@ -34,7 +34,11 @@ export type { } from "./vocabulary.js"; // --- Schema ---------------------------------------------------------------- -export { enforcedProvidersConfigSchema, providersConfigSchema } from "./schema.js"; +export { + customModelSchema, + enforcedProvidersConfigSchema, + providersConfigSchema, +} from "./schema.js"; // --- Types (inferred + catalog) -------------------------------------------- export { From dbbdeefa44f670202d4e580a05c3ae5c1321bfd5 Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Wed, 15 Jul 2026 14:55:22 -0400 Subject: [PATCH 7/8] Document capability inference policy and fixed provider cases --- memory-bank/aiConfig.md | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/memory-bank/aiConfig.md b/memory-bank/aiConfig.md index 445fd51..69cdec8 100644 --- a/memory-bank/aiConfig.md +++ b/memory-bank/aiConfig.md @@ -204,7 +204,15 @@ and Gemma tables, since Posit AI routes both families. **`inferModelCapabilities(providerId, modelId)`** (`src/model-capabilities/infer.ts`) merges a conservative `GENERIC_BASELINE` (128k context, tools on, no images, no web search) under provider-family inference, with inference winning per -field: +field. + +The policy for every provider case: mirror the static caps the bridge's +provider builder declares for that provider. Where a builder derives values +from live API responses (e.g. Vertex token limits), inference approximates +with the family table and keeps feature flags conservative — understating a +capability degrades gracefully, overstating it breaks requests. + +Per-provider cases: - `anthropic` / `bedrock` → the Anthropic table (Bedrock ids carry the same `claude-*` family, just prefixed). @@ -217,12 +225,22 @@ field: separate context-window figure, so `maxContextLength` is set equal to the table's `maxInputTokens` (mirroring how `deepseek-provider.ts` in the bridge already treats the input limit as the window). -- `snowflake-cortex` → tries the Anthropic table first (Claude on Snowflake - speaks the Anthropic Messages API); if that misses, strips a leading - `openai-` prefix and tries the OpenAI table. Either branch sets `protocol` - (`"anthropic-messages"` or `"openai-chat"`) — the only case where inference - determines the wire protocol. Every other provider leaves `protocol` - `undefined`. +- `google-vertex` → strips Vertex resource prefixes + (`publishers//models/...`), then routes Gemini ids to the Gemini + table and Anthropic partner ids to the Anthropic table (mirroring + `google-vertex-provider.ts`; the builder's live-API token limits match the + Gemini table's 1M/65k values). +- `snowflake-cortex` → Snowflake serves a fixed catalog with its own caps, so + inference applies them rather than the upstream model limits: Claude ids get + the Anthropic Messages API shape (200k context / 16,384 output, tool-result + images on); anything else strips a leading `openai-` prefix, consults the + OpenAI table, and gets the Chat Completions shape (128k / 16,384, + tool-result images off, image support only when the upstream table lists + image media types). Only `family` and `thinkingEffortLevels` are borrowed + from the upstream tables (mirroring `snowflake-cortex-provider.ts`). Both + branches set `protocol` (`"anthropic-messages"` or `"openai-chat"`) — the + only case where inference determines the wire protocol. Every other + provider leaves `protocol` `undefined`. - Anything else (`ms-foundry`, `openai-compatible`, custom provider ids) stays at the generic baseline — those are unknown endpoints. From 8c19152d5569aecc1f8418f7e8d0bf8909a14d2e Mon Sep 17 00:00:00 2001 From: Melissa Barca Date: Thu, 16 Jul 2026 09:36:14 -0400 Subject: [PATCH 8/8] Drop requiresChatTemplateKwargs from inferred caps; fix README --- packages/ai-config/README.md | 4 ++- .../__tests__/infer.test.ts | 35 +++++++++++++++++-- .../ai-config/src/model-capabilities/infer.ts | 14 ++++++-- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/ai-config/README.md b/packages/ai-config/README.md index 0e7636b..78c35e9 100644 --- a/packages/ai-config/README.md +++ b/packages/ai-config/README.md @@ -157,7 +157,9 @@ import type { InferredModelCapabilities } from "ai-config"; The per-provider `get*ModelCapabilities(modelId)` helpers are pure, regex-driven lookups that map a provider-specific model id to a `Partial` (or `undefined`/a default object when the id doesn't match that provider's family). `openaiMaxInputTokens(caps)` derives the OpenAI input-token ceiling from a capability object's context window and output-token reservation. -`inferModelCapabilities(providerId, modelId): InferredModelCapabilities` is the single entry point that ties the tables together: it merges a conservative generic baseline (128k context, tools on, no images, no web search) under whichever provider-family table applies, with the table's values winning per field. It also derives `supportsImages` from a table's `supportedInputMediaTypes` when the table sets media types but leaves the flag itself unset, and resolves `protocol` for `snowflake-cortex` ids (Claude ids → `"anthropic-messages"`, everything else → `"openai-chat"` after stripping a leading `openai-` prefix). This is the function `ai-provider-bridge` re-exports at its root, and the intended delegation target for any consumer that needs model capabilities without the bridge's dependency tree. +`inferModelCapabilities(providerId, modelId)` is the single entry point that ties the tables together: it merges a conservative generic baseline (128k context, tools on, no images, no web search) under whichever provider-family table applies, with the table's values winning per field. It also derives `supportsImages` from a table's `supportedInputMediaTypes` when the table sets media types but leaves the flag itself unset, and resolves `protocol` for `snowflake-cortex` ids (Claude ids → `"anthropic-messages"`, everything else → `"openai-chat"` after stripping a leading `openai-` prefix). The result is shaped to spread straight into a `models.custom` entry, so it omits `requiresChatTemplateKwargs` (a runtime-only flag the strict custom-model schema rejects). It's the intended delegation target for any consumer that needs model capabilities without the bridge's dependency tree. + +`ai-provider-bridge` re-exports the per-provider `get*ModelCapabilities` helpers and `openaiMaxInputTokens` from its own root for existing consumers, but not `inferModelCapabilities` — import that from `ai-config` directly. --- diff --git a/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts index 1ad48de..45a7fd5 100644 --- a/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts +++ b/packages/ai-config/src/model-capabilities/__tests__/infer.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest"; +import { customModelSchema } from "../../schema.js"; import { inferModelCapabilities } from "../infer.js"; describe("inferModelCapabilities", () => { @@ -76,12 +77,42 @@ describe("inferModelCapabilities", () => { expect(inferModelCapabilities("anthropic", "claude-sonnet-4-5").protocol).toBeUndefined(); }); - it("passes gemma flags through the positai family", () => { + it("passes gemma thinking levels through the positai family", () => { const caps = inferModelCapabilities("positai", "google/gemma-4-27b-it"); - expect(caps.requiresChatTemplateKwargs).toBe(true); expect(caps.thinkingEffortLevels).toEqual(["off", "on"]); }); + it("omits requiresChatTemplateKwargs so the result fits a models.custom entry", () => { + // The Gemma table sets this runtime-only flag, but the strict custom-model + // schema rejects it; inferModelCapabilities must not surface it. + const caps = inferModelCapabilities("positai", "google/gemma-4-27b-it"); + expect(caps).not.toHaveProperty("requiresChatTemplateKwargs"); + }); + + it("produces a spread that validates against the strict customModelSchema", () => { + // The migration use case: { id, name, ...inferModelCapabilities(...) } must + // parse. Gemma is the regression case — its table sets a key the schema + // rejects. Cover a representative id per provider family. + for (const [providerId, modelId] of [ + ["positai", "google/gemma-4-27b-it"], + ["anthropic", "claude-opus-4-8"], + ["openai", "gpt-4o"], + ["gemini", "gemini-2.5-pro"], + ["deepseek", "deepseek-chat"], + ["snowflake-cortex", "claude-sonnet-4-5"], + ["snowflake-cortex", "openai-gpt-5.2"], + ["google-vertex", "gemini-2.5-pro"], + ["openai-compatible", "totally-unknown-model"], + ] as const) { + const result = customModelSchema.safeParse({ + id: modelId, + name: modelId, + ...inferModelCapabilities(providerId, modelId), + }); + expect(result.success, `${providerId}/${modelId}: ${result.error?.message}`).toBe(true); + } + }); + it("caps snowflake claude ids to Snowflake's limits, not the upstream table", () => { // The Anthropic table gives Opus 4.7 a 1M window / 128k output; Snowflake // Cortex serves it at 200k / 16k (snowflake-cortex-provider.ts). diff --git a/packages/ai-config/src/model-capabilities/infer.ts b/packages/ai-config/src/model-capabilities/infer.ts index 3cbe7a6..f2d986c 100644 --- a/packages/ai-config/src/model-capabilities/infer.ts +++ b/packages/ai-config/src/model-capabilities/infer.ts @@ -140,12 +140,22 @@ function inferProviderDefaults( * per field). Every required capability field is always present; optional * fields (family, media types, thinking levels, protocol) appear only when * inference determined them. + * + * The result is shaped to spread directly into a `models.custom` entry, so it + * excludes `requiresChatTemplateKwargs`: that flag is a runtime request-shaping + * detail (it tells the vLLM client to send `chat_template_kwargs`), re-derived + * from the model id at request time by the bridge's positai path, and it is not + * a field the strict `customModelSchema` accepts. The capability tables still + * carry it for that runtime use; it is dropped only here, at the migration seam. */ export function inferModelCapabilities( providerId: string, modelId: string, -): InferredModelCapabilities { - const inferred = inferProviderDefaults(providerId, modelId); +): Omit { + const { requiresChatTemplateKwargs: _drop, ...inferred } = inferProviderDefaults( + providerId, + modelId, + ); // `??` per required field (not a bare spread) so a helper that explicitly // sets a field to `undefined` cannot shadow the baseline. return {