diff --git a/docs/providers/github-copilot.md b/docs/providers/github-copilot.md index ccb091282035..c03c5f0b4c01 100644 --- a/docs/providers/github-copilot.md +++ b/docs/providers/github-copilot.md @@ -289,8 +289,9 @@ configured default model is never replaced. Claude model IDs use the Anthropic Messages transport automatically. Gemini models use the OpenAI Chat Completions transport; GPT and o-series - models keep the OpenAI Responses transport. OpenClaw selects the correct - transport based on the model ref. + models keep the OpenAI Responses transport. The bundled static catalog + includes these transports and request compatibility settings, so Gemini + keeps using Chat Completions when live discovery is disabled or unavailable. diff --git a/extensions/github-copilot/model-metadata.ts b/extensions/github-copilot/model-metadata.ts index 67d21f4dfafd..ec33d24d44de 100644 --- a/extensions/github-copilot/model-metadata.ts +++ b/extensions/github-copilot/model-metadata.ts @@ -26,10 +26,6 @@ const manifestModels = buildManifestModelProviderConfig({ providerId: "github-copilot", catalog: manifestCatalog, }).models; -for (const model of manifestModels) { - model.api = resolveCopilotTransportApi(model.id); - model.compat = { ...resolveCopilotModelCompat(model.id), ...model.compat }; -} const STATIC_MODEL_OVERRIDES = new Map>([ ...manifestModels.map((model) => [model.id, model] as const), diff --git a/extensions/github-copilot/openclaw.plugin.json b/extensions/github-copilot/openclaw.plugin.json index dc4e60aed25f..0f194d387434 100644 --- a/extensions/github-copilot/openclaw.plugin.json +++ b/extensions/github-copilot/openclaw.plugin.json @@ -97,6 +97,13 @@ { "id": "gemini-3.6-flash", "name": "Gemini 3.6 Flash", + "api": "openai-completions", + "compat": { + "supportsStore": false, + "supportsDeveloperRole": false, + "supportsUsageInStreaming": false, + "maxTokensField": "max_tokens" + }, "input": ["text", "image"], "contextWindow": 1048576, "maxTokens": 65536, @@ -105,6 +112,13 @@ { "id": "gemini-3.1-pro-preview", "name": "Gemini 3.1 Pro Preview", + "api": "openai-completions", + "compat": { + "supportsStore": false, + "supportsDeveloperRole": false, + "supportsUsageInStreaming": false, + "maxTokensField": "max_tokens" + }, "input": ["text", "image"], "contextWindow": 1048576, "maxTokens": 65536, @@ -113,6 +127,13 @@ { "id": "gemini-3.5-flash", "name": "Gemini 3.5 Flash", + "api": "openai-completions", + "compat": { + "supportsStore": false, + "supportsDeveloperRole": false, + "supportsUsageInStreaming": false, + "maxTokensField": "max_tokens" + }, "status": "deprecated", "replacedBy": "gemini-3.6-flash", "input": ["text", "image"], @@ -123,6 +144,13 @@ { "id": "gemini-2.5-pro", "name": "Gemini 2.5 Pro", + "api": "openai-completions", + "compat": { + "supportsStore": false, + "supportsDeveloperRole": false, + "supportsUsageInStreaming": false, + "maxTokensField": "max_tokens" + }, "status": "deprecated", "replacedBy": "gemini-3.1-pro-preview", "input": ["text", "image"], diff --git a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts index 988055342219..ae6dbaeeeaac 100644 --- a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts @@ -1,5 +1,7 @@ // Provider runtime contract helpers define reusable runtime tests for provider plugins. +import { normalizeModelCatalog } from "@openclaw/model-catalog-core/model-catalog-normalize"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { createPluginMetadataSnapshot } from "../../config/plugin-auto-enable.test-helpers.js"; import type { ProviderRuntimeModel } from "../plugin-entry.js"; import { registerProviderPlugin, requireRegisteredProvider } from "../plugin-test-runtime.js"; import { buildManifestModelProviderConfig } from "../provider-catalog-shared.js"; @@ -283,6 +285,95 @@ export function describeGithubCopilotProviderRuntimeContract( ]); const createManifestModel = createManifestModelFactory("github-copilot", manifestCatalog); + it.each([ + ["gemini-3.6-flash", "openai-completions", false], + ["gemini-3.1-pro-preview", "openai-completions", false], + ["gemini-3.5-flash", "openai-completions", false], + ["gemini-2.5-pro", "openai-completions", false], + ["gpt-5.6-sol", "openai-responses", false], + ["claude-sonnet-5", "anthropic-messages", false], + ["gemini-3.6-flash", "openai-completions", true], + ] as const)( + "routes static %s through %s with discovery enabled=%s", + async (modelId, api, discoveryEnabled) => { + const { createBundledStaticCatalogModelResolver } = + await import("../../agents/embedded-agent-runner/model.static-catalog.js"); + const config = { + models: { catalogRefresh: { enabled: false } }, + plugins: { + entries: { + "github-copilot": { config: { discovery: { enabled: discoveryEnabled } } }, + }, + }, + }; + const metadataSnapshot = createPluginMetadataSnapshot({ + config, + manifestRegistry: { + diagnostics: [], + plugins: [ + { + id: "github-copilot", + origin: "bundled", + providers: ["github-copilot"], + channels: [], + cliBackends: [], + skills: [], + hooks: [], + rootDir: "/fixtures/github-copilot", + source: "/fixtures/github-copilot/index.js", + manifestPath: "/fixtures/github-copilot/openclaw.plugin.json", + modelCatalog: normalizeModelCatalog( + { + providers: { "github-copilot": manifestCatalog }, + discovery: { "github-copilot": "runtime" }, + }, + { ownedProviders: new Set(["github-copilot"]) }, + ), + }, + ], + }, + }); + const resolveModel = createBundledStaticCatalogModelResolver({ + cfg: config, + env: {}, + metadataSnapshot, + includeRuntimeDiscovery: true, + }); + const model = resolveModel({ provider: "github-copilot", modelId }); + expect(model?.api).toBe(api); + if (api === "openai-completions") { + expect(model?.compat).toMatchObject({ + supportsStore: false, + supportsDeveloperRole: false, + supportsUsageInStreaming: false, + maxTokensField: "max_tokens", + }); + } + const provider = requireProviderContractProvider("github-copilot"); + expect( + provider.preferRuntimeResolvedModel?.({ + config, + provider: "github-copilot", + modelId, + }), + ).toBe(discoveryEnabled); + // A missing prepared live row also occurs when enabled discovery is unavailable. + expect( + provider.resolveDynamicModel?.({ + config, + provider: "github-copilot", + modelId, + modelRegistry: { + find: () => model, + getAll: () => (model ? [model] : []), + getAvailable: () => (model ? [model] : []), + hasConfiguredAuth: () => false, + }, + }), + ).toBeUndefined(); + }, + ); + it("owns Copilot-specific forward-compat fallbacks", () => { const provider = requireProviderContractProvider("github-copilot"); const expected = createManifestModel("gpt-5.4");