{"version":3,"file":"anthropic-discovery.d.ts","sourceRoot":"","sources":["../../src/providers/anthropic-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAiB9C;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,2DAA2D;AAC3D,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD;AA8OD,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,IAAI,CAE1F","sourcesContent":["/**\n * Model-capability discovery for Anthropic-family models.\n *\n * Two backends, dispatched by provider:\n *\n *  - github-copilot: GET {baseUrl}/models — OpenAI-style listing with rich\n *    `capabilities.supports` (adaptive_thinking, reasoning_effort, limits.\n *    max_context_window_tokens). The 1M context tier on Copilot is exposed\n *    as separate model ids (for example claude-opus-4.6-1m) rather than\n *    via a beta header; the bare id (claude-opus-4.6) is hard-capped at\n *    200k regardless of any beta header (which the relay rejects).\n *    Discovery surfaces every id the response returns so the user can\n *    select the right one in the picker.\n *\n *  - anthropic: GET {baseUrl}/v1/models — Anthropic-native with\n *    `capabilities.thinking.types.adaptive.supported`,\n *    `capabilities.effort.{max,xhigh}.supported`, `max_input_tokens`.\n *    The 1M context tier is gated by `anthropic-beta: context-1m-2025-08-07`\n *    on the same id; we detect it by making a second probe with that\n *    header and observing whether `max_input_tokens` rises.\n *\n *  - others (amazon-bedrock, google-vertex, etc.): no equivalent\n *    discovery endpoint; the static fallback table in\n *    anthropic-capabilities.ts is used.\n *\n * Discovery runs at most once per (provider, baseUrl, apiKey-hash) per\n * process. Concurrent calls join the same in-flight promise. Failures are\n * silent — we leave the static fallback in place and try again next\n * process. The discovery layer never throws back to the caller.\n *\n * The discovered registry entries (new model ids, corrected contextWindow)\n * are also pushed into the in-memory model registry via\n * `mergeDiscoveredModels` so the model picker reflects reality.\n */\n\nimport type { Api, Model } from \"../types.js\";\nimport {\n\ttype AnthropicModelCapabilities,\n\tCONTEXT_1M_BETA_HEADER,\n\tsetDiscoveredCapabilities,\n} from \"./anthropic-capabilities.js\";\n\n// ----------------------------------------------------------------------------\n// Discovery state (per process)\n// ----------------------------------------------------------------------------\n\nconst discoveryPromises = new Map<string, Promise<void>>();\n\nfunction discoveryKey(provider: string, baseUrl: string): string {\n\treturn `${provider}::${baseUrl}`;\n}\n\n/**\n * Trigger discovery for a provider+baseUrl. Returns a Promise that resolves\n * once the capability cache has been populated (or the attempt has failed).\n * Subsequent calls with the same (provider, baseUrl) join the in-flight\n * promise.\n *\n * Never throws. On any error the static fallback remains in place.\n */\nexport function discoverAnthropicCapabilities(\n\tprovider: string,\n\tbaseUrl: string,\n\tapiKey: string,\n\textraHeaders?: Record<string, string>,\n): Promise<void> {\n\tconst key = discoveryKey(provider, baseUrl);\n\tconst existing = discoveryPromises.get(key);\n\tif (existing) return existing;\n\n\tconst p = runDiscovery(provider, baseUrl, apiKey, extraHeaders).catch(() => {\n\t\t// Swallow — discovery is best-effort; static table remains in place.\n\t});\n\tdiscoveryPromises.set(key, p);\n\treturn p;\n}\n\n/** Test-only: clear discovery memo so tests can re-run. */\nexport function _clearDiscoveryStateForTests(): void {\n\tdiscoveryPromises.clear();\n}\n\n// ----------------------------------------------------------------------------\n// Dispatcher\n// ----------------------------------------------------------------------------\n\nasync function runDiscovery(\n\tprovider: string,\n\tbaseUrl: string,\n\tapiKey: string,\n\textraHeaders?: Record<string, string>,\n): Promise<void> {\n\tif (provider === \"github-copilot\") {\n\t\tawait discoverCopilot(baseUrl, apiKey, extraHeaders);\n\t\treturn;\n\t}\n\tif (provider === \"anthropic\") {\n\t\tawait discoverAnthropicNative(baseUrl, apiKey, extraHeaders);\n\t\treturn;\n\t}\n\t// No discovery endpoint for other providers (Bedrock, Vertex, etc.).\n}\n\n// ----------------------------------------------------------------------------\n// GitHub Copilot: GET {baseUrl}/models\n// ----------------------------------------------------------------------------\n\ninterface CopilotModelEntry {\n\tid: string;\n\tname?: string;\n\tvendor?: string;\n\tsupported_endpoints?: string[];\n\tmodel_picker_enabled?: boolean;\n\tcapabilities?: {\n\t\tfamily?: string;\n\t\ttype?: string;\n\t\tsupports?: {\n\t\t\tadaptive_thinking?: boolean;\n\t\t\treasoning_effort?: string[];\n\t\t\tstreaming?: boolean;\n\t\t\ttool_calls?: boolean;\n\t\t\tvision?: boolean;\n\t\t};\n\t\tlimits?: {\n\t\t\tmax_context_window_tokens?: number;\n\t\t\tmax_output_tokens?: number;\n\t\t\tmax_prompt_tokens?: number;\n\t\t};\n\t};\n}\n\nasync function discoverCopilot(baseUrl: string, apiKey: string, extraHeaders?: Record<string, string>): Promise<void> {\n\tconst res = await fetch(`${baseUrl}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\t\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\t\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n\tif (!res.ok) return;\n\tconst body = (await res.json()) as { data?: CopilotModelEntry[] };\n\tconst entries = body?.data ?? [];\n\n\tconst discoveredModels: Model<\"anthropic-messages\">[] = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.vendor !== \"Anthropic\") continue;\n\t\tif (!entry.supported_endpoints?.includes(\"/v1/messages\")) continue;\n\n\t\tconst caps = entry.capabilities ?? {};\n\t\tconst sup = caps.supports ?? {};\n\t\tconst lim = caps.limits ?? {};\n\n\t\tconst adaptive = sup.adaptive_thinking === true;\n\t\tconst efforts = sup.reasoning_effort ?? [];\n\t\tconst xhighEffort = efforts.includes(\"max\") || efforts.includes(\"xhigh\");\n\n\t\tsetDiscoveredCapabilities(\"github-copilot\", entry.id, {\n\t\t\tthinkingSchema: adaptive ? \"adaptive\" : \"legacy\",\n\t\t\txhighEffort: xhighEffort || undefined,\n\t\t\tcontextWindow: lim.max_context_window_tokens,\n\t\t});\n\n\t\t// Build a registry entry so previously-unknown ids (e.g.\n\t\t// claude-opus-4.6-1m) show up in the model picker.\n\t\tdiscoveredModels.push({\n\t\t\tid: entry.id,\n\t\t\tname: entry.name || entry.id,\n\t\t\tapi: \"anthropic-messages\",\n\t\t\tprovider: \"github-copilot\",\n\t\t\tbaseUrl,\n\t\t\theaders: {\n\t\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t\t\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\t\t\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\t\t\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\t\t},\n\t\t\treasoning: adaptive,\n\t\t\tinput: sup.vision ? [\"text\", \"image\"] : [\"text\"],\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\tcontextWindow: lim.max_context_window_tokens ?? 128000,\n\t\t\tmaxTokens: lim.max_output_tokens ?? 8192,\n\t\t});\n\t}\n\n\tmergeDiscoveredModels(\"github-copilot\", discoveredModels);\n}\n\n// ----------------------------------------------------------------------------\n// Direct Anthropic: GET {baseUrl}/v1/models\n// ----------------------------------------------------------------------------\n\ninterface AnthropicCapabilitySupport {\n\tsupported?: boolean;\n}\ninterface AnthropicModelEntry {\n\tid: string;\n\tdisplay_name?: string;\n\tmax_input_tokens?: number;\n\tmax_tokens?: number;\n\tcapabilities?: {\n\t\tthinking?: {\n\t\t\tsupported?: boolean;\n\t\t\ttypes?: {\n\t\t\t\tadaptive?: AnthropicCapabilitySupport;\n\t\t\t\tenabled?: AnthropicCapabilitySupport;\n\t\t\t};\n\t\t};\n\t\teffort?: {\n\t\t\tsupported?: boolean;\n\t\t\tlow?: AnthropicCapabilitySupport;\n\t\t\tmedium?: AnthropicCapabilitySupport;\n\t\t\thigh?: AnthropicCapabilitySupport;\n\t\t\tmax?: AnthropicCapabilitySupport;\n\t\t\txhigh?: AnthropicCapabilitySupport;\n\t\t};\n\t\timage_input?: AnthropicCapabilitySupport;\n\t};\n}\n\nasync function discoverAnthropicNative(\n\tbaseUrl: string,\n\tapiKey: string,\n\textraHeaders?: Record<string, string>,\n): Promise<void> {\n\tconst baseHeaders: Record<string, string> = {\n\t\t\"x-api-key\": apiKey,\n\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t...extraHeaders,\n\t};\n\n\t// First, capability map from the bare /v1/models endpoint.\n\tconst baseEntries = await fetchAnthropicModels(baseUrl, baseHeaders);\n\tif (!baseEntries) return;\n\n\t// Then, retry with the context-1m beta and compare max_input_tokens.\n\t// Models whose max_input_tokens grows with the beta need it to unlock\n\t// the larger window; others reject the beta or ignore it.\n\tconst betaEntries = await fetchAnthropicModels(baseUrl, {\n\t\t...baseHeaders,\n\t\t\"anthropic-beta\": CONTEXT_1M_BETA_HEADER,\n\t});\n\tconst betaWindowById = new Map<string, number>();\n\tif (betaEntries) {\n\t\tfor (const entry of betaEntries) {\n\t\t\tif (typeof entry.max_input_tokens === \"number\") {\n\t\t\t\tbetaWindowById.set(entry.id, entry.max_input_tokens);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const entry of baseEntries) {\n\t\tconst c = entry.capabilities ?? {};\n\t\tconst thinking = c.thinking ?? {};\n\t\tconst types = thinking.types ?? {};\n\t\tconst effort = c.effort ?? {};\n\n\t\tconst adaptive = types.adaptive?.supported === true;\n\t\tconst xhighEffort = effort.max?.supported === true || effort.xhigh?.supported === true;\n\n\t\tconst baseWindow = entry.max_input_tokens ?? 0;\n\t\tconst betaWindow = betaWindowById.get(entry.id) ?? 0;\n\t\tconst wantsBeta = betaWindow > baseWindow;\n\n\t\tconst caps: AnthropicModelCapabilities = {\n\t\t\tthinkingSchema: adaptive ? \"adaptive\" : \"legacy\",\n\t\t};\n\t\tif (xhighEffort) caps.xhighEffort = true;\n\t\tif (wantsBeta) {\n\t\t\tcaps.contextBeta = CONTEXT_1M_BETA_HEADER;\n\t\t\tcaps.contextWindow = betaWindow;\n\t\t} else if (baseWindow > 0) {\n\t\t\tcaps.contextWindow = baseWindow;\n\t\t}\n\n\t\tsetDiscoveredCapabilities(\"anthropic\", entry.id, caps);\n\t}\n}\n\nasync function fetchAnthropicModels(\n\tbaseUrl: string,\n\theaders: Record<string, string>,\n): Promise<AnthropicModelEntry[] | null> {\n\tconst url = `${baseUrl.replace(/\\/$/, \"\")}/v1/models?limit=1000`;\n\tconst res = await fetch(url, { method: \"GET\", headers });\n\tif (!res.ok) return null;\n\tconst body = (await res.json()) as { data?: AnthropicModelEntry[] };\n\treturn body?.data ?? null;\n}\n\n// ----------------------------------------------------------------------------\n// Registry merge\n// ----------------------------------------------------------------------------\n\n/**\n * Push freshly-discovered models into the in-memory registry via the hook\n * wired by models.ts. New ids are inserted; existing ids have their\n * capability-related fields refreshed.\n */\nfunction mergeDiscoveredModels(provider: string, fresh: Model<Api>[]): void {\n\tfor (const m of fresh) {\n\t\tregisterModel(provider, m);\n\t}\n}\n\n// Registry mutation is provided by models.ts at module load time to avoid a\n// circular import. Until it is wired, discovery still populates the\n// capability cache but cannot publish new model ids into the registry.\nconst registryHookHolder: { fn: (provider: string, model: Model<Api>) => void } = { fn: () => {} };\n\nfunction registerModel(provider: string, model: Model<Api>): void {\n\tregistryHookHolder.fn(provider, model);\n}\n\n/** Not for external use: called by models.ts at module load to wire the registry hook. */\nexport function _setRegistryHook(hook: (provider: string, model: Model<Api>) => void): void {\n\tregistryHookHolder.fn = hook;\n}\n"]}