diff --git a/.env.example b/.env.example index cf05680a..f8a63e24 100644 --- a/.env.example +++ b/.env.example @@ -125,7 +125,7 @@ OPENAI_API_KEY= # # OPENAI_BASE_URL=https://gateway.internal/v1 # OPENAI_API_KEY=... -# BOT_MODEL=openai/gpt-4o +# BOT_MODEL=openai/gpt-5.6-terra # # OPENAI_BASE_URL= @@ -141,11 +141,12 @@ OPENAI_API_KEY= # ANTHROPIC_API_KEY= # GOOGLE_API_KEY= -# Which model. Defaults per provider: gpt-5.5, claude-sonnet-4-5, gemini-2.5-flash. -# BOT_MODEL=gpt-5.5 +# Which model. Defaults per provider: gpt-5.6-terra, claude-sonnet-4-5, gemini-2.5-flash. +# OpenAI's 5.6 tiers are sol (most capable), terra (the default here) and luna (cheapest). +# BOT_MODEL=gpt-5.6-terra -# OpenAI only. Its newer models require the Responses API, which the framework Bot handles and the -# proof-of-concept one cannot. +# OpenAI only, and rarely needed: the framework Bot turns the Responses API on by itself for models +# that require it. Set it when you are using a model this build has not heard of that needs it too. # BOT_RESPONSES_API=false # The Bot computer. Absent means the feature is off and its routes are not mounted. @@ -237,8 +238,10 @@ MANAGED_AGENT_TOKEN= # proof of concept, and is reached the same way: point MANAGED_AGENT_AG_UI_URL at it, or add it as a # Bot of its own in the tenant package or at /agents. -# Which model the Bots use. agent-bot speaks /v1/chat/completions and cannot use gpt-5.6-*, which -# require the Responses API. agent-langgraph can: set BOT_RESPONSES_API=true and give it a 5.6 model. +# Which model the Bots use. agent-langgraph runs gpt-5.6-terra and switches to the Responses API by +# itself, because 5.6 rejects function tools on /v1/chat/completions. agent-bot speaks that endpoint +# by hand and stays on gpt-5.5: the alternative there is reasoning_effort 'none', and a Bot that has +# to decide when to ask a person for help should not be the one with its reasoning turned off. # BOT_RESPONSES_API=false # One computer per Bot. Unset, every Bot shares the computer at AGENT_COMPUTER_URL, suitable on a diff --git a/agent-langgraph/src/index.ts b/agent-langgraph/src/index.ts index 3d183e1a..b8a697d6 100644 --- a/agent-langgraph/src/index.ts +++ b/agent-langgraph/src/index.ts @@ -24,9 +24,10 @@ import { toLangChainMessages } from "./history"; * server changes. * * The model API stops being ours. `agent-bot` speaks `/v1/chat/completions` by hand, which is why - * gpt-5.6 cannot be used there without rewriting its streaming loop: those models reject function - * tools on that endpoint and require the Responses API. Here it is `useResponsesApi`, one line, - * because the migration belongs to the people who maintain the integration. + * gpt-5.6 costs it a rewritten streaming loop: those models reject function tools on that endpoint + * unless reasoning is turned off, and turning reasoning off on a Bot that has to decide when to ask + * a person for help is the wrong trade. Here it is `useResponsesApi`, one line, because the + * migration belongs to the people who maintain the integration. * * The tool loop still runs on the client, exactly as it does in `agent-bot`: a Bot's actions happen * on a browser the person is watching, and the surface remains the place that executes those tools. @@ -66,8 +67,17 @@ const PROVIDER = (process.env.BOT_PROVIDER ?? "openai").toLowerCase(); * missing configuration. */ const MODEL = process.env.BOT_MODEL?.trim() || defaultModelFor(PROVIDER); -/** OpenAI only. Its newer models require the Responses API, which the integration handles. */ -const USE_RESPONSES_API = process.env.BOT_RESPONSES_API === "true"; +/** + * OpenAI only. Its newer models require the Responses API, which the integration handles. + * + * Inferred from the model rather than left to a separate switch. `gpt-5.6-*` rejects function tools + * on `/v1/chat/completions`, so a deployment that set `BOT_MODEL` to one and did not also know about + * this flag got a Bot that started, looked healthy, and failed on its first tool call. The switch is + * still honoured, so a model this list has not heard of can be told to use it. + */ +const NEEDS_RESPONSES_API = /^gpt-5\.[6-9]|^gpt-[6-9]/.test(MODEL); +const USE_RESPONSES_API = + process.env.BOT_RESPONSES_API === "true" || NEEDS_RESPONSES_API; /** * OpenAI only, and the same variable the API server reads for its built-in agents. * @@ -90,7 +100,7 @@ const GOOGLE_BASE_URL = function defaultModelFor(provider: string): string { if (provider === "anthropic") return "claude-sonnet-4-5"; if (provider === "google") return "gemini-2.5-flash"; - return "gpt-5.5"; + return "gpt-5.6-terra"; } /** diff --git a/docker-compose.yml b/docker-compose.yml index 4c67db89..bbe2a643 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -177,7 +177,7 @@ services: # Unset means OpenAI. Set, it is any endpoint speaking the same API, and BOT_MODEL is sent # to it verbatim. OPENAI_BASE_URL: ${OPENAI_BASE_URL:-} - BOT_MODEL: ${BOT_MODEL:-gpt-5.5} + BOT_MODEL: ${BOT_MODEL:-gpt-5.6-terra} healthcheck: test: ["CMD-SHELL", "bun -e \"await fetch('http://localhost:4200/health')\""] interval: 10s @@ -204,7 +204,7 @@ services: ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-} GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} GOOGLE_GENERATIVE_AI_BASE_URL: ${GOOGLE_GENERATIVE_AI_BASE_URL:-} - BOT_MODEL: ${BOT_MODEL:-gpt-5.5} + BOT_MODEL: ${BOT_MODEL:-gpt-5.6-terra} BOT_RESPONSES_API: ${BOT_RESPONSES_API:-false} # Where this Bot runs a tool: back through the deployment that granted it, never at the vendor. # `host.docker.internal` because the API server runs on the host, not in this network. diff --git a/docs/configuration.md b/docs/configuration.md index 95adf337..4943c734 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -93,7 +93,7 @@ A gateway that fronts several providers behind one key is addressed the usual wa ```sh OPENAI_BASE_URL=https://gateway.internal/v1 OPENAI_API_KEY=... -BOT_MODEL=openai/gpt-4o +BOT_MODEL=openai/gpt-5.6-terra ``` and in the tenant package, where the name is namespaced the same way: @@ -102,7 +102,7 @@ and in the tenant package, where the name is namespaced the same way: model: provider: openai credential_secret_ref: openai-api-key - default_model: openai/gpt-4o + default_model: openai/gpt-5.6-terra ``` Most gateways publish a model list, which is the way to check a name before configuring it. @@ -366,7 +366,7 @@ column nor `users.groups` is the wrong shape for it. model: provider: openai credential_secret_ref: openai-api-key - default_model: gpt-4.1 + default_model: gpt-5.6-terra ``` `provider` must be `openai`. `credential_secret_ref` is a reference to a stored credential, not a credential value. `default_model` is passed through as written, so an OpenAI-compatible endpoint reached through `OPENAI_BASE_URL` takes the name that endpoint publishes. diff --git a/examples/fintech/model.yaml b/examples/fintech/model.yaml index 5bdce6a2..2c0d518a 100644 --- a/examples/fintech/model.yaml +++ b/examples/fintech/model.yaml @@ -1,4 +1,4 @@ model: provider: openai credential_secret_ref: openai-api-key - default_model: gpt-4.1 + default_model: gpt-5.6-terra diff --git a/examples/langgraph-bot/src/index.ts b/examples/langgraph-bot/src/index.ts index 6f3a1b5b..5a96f544 100644 --- a/examples/langgraph-bot/src/index.ts +++ b/examples/langgraph-bot/src/index.ts @@ -27,7 +27,15 @@ import { serve } from "bun"; */ const PORT = Number.parseInt(process.env.PORT ?? "4300", 10); -const MODEL = process.env.BOT_MODEL ?? "gpt-5.5"; +const MODEL = process.env.BOT_MODEL ?? "gpt-5.6-terra"; +/** + * `gpt-5.6-*` rejects function tools on `/v1/chat/completions` and needs the Responses API, which + * this integration speaks. Inferred from the model so setting `BOT_MODEL` alone cannot produce a + * Bot that starts, looks healthy, and fails on its first tool call. + */ +const USE_RESPONSES_API = + process.env.BOT_RESPONSES_API === "true" || + /^gpt-5\.[6-9]|^gpt-[6-9]/.test(MODEL); /** * The graph's state. @@ -131,7 +139,11 @@ function buildGraph(input: RunAgentInput) { // the graph returns once it is finished, and the surface receives a single TEXT_MESSAGE_CONTENT // carrying all of it, so a six-hundred-word reply is a blank conversation for eight seconds and // then a wall of text. The tokens exist the whole time; nothing was passing them on. - const model = new ChatOpenAI({ model: MODEL, streaming: true }); + const model = new ChatOpenAI({ + model: MODEL, + streaming: true, + ...(USE_RESPONSES_API ? { useResponsesApi: true } : {}), + }); const tools = toLangChainTools(input); const bound = tools.length > 0 ? model.bindTools(tools) : model; diff --git a/examples/mastra-bot/src/index.ts b/examples/mastra-bot/src/index.ts index c5d5f715..0d99f3e8 100644 --- a/examples/mastra-bot/src/index.ts +++ b/examples/mastra-bot/src/index.ts @@ -17,7 +17,15 @@ import { serve } from "bun"; */ const PORT = Number.parseInt(process.env.PORT ?? "4400", 10); -const MODEL = process.env.BOT_MODEL ?? "gpt-5.5"; +const MODEL = process.env.BOT_MODEL ?? "gpt-5.6-terra"; +/** + * `gpt-5.6-*` rejects function tools on `/v1/chat/completions` and needs the Responses API, which + * this provider exposes as `openai.responses`. Inferred from the model so setting `BOT_MODEL` alone + * cannot produce a Bot that starts, looks healthy, and fails on its first tool call. + */ +const NEEDS_RESPONSES_API = + process.env.BOT_RESPONSES_API === "true" || + /^gpt-5\.[6-9]|^gpt-[6-9]/.test(MODEL); const bot = new Agent({ name: "OpenBot Mastra coworker", @@ -28,7 +36,7 @@ const bot = new Agent({ "NEVER state what a page contains unless you have just read it with a tool in this conversation. " + "You cannot know a page's contents from memory. If you have not read it, call the tool first, " + "and report exactly what the tool returned.", - model: openai(MODEL), + model: NEEDS_RESPONSES_API ? openai.responses(MODEL) : openai(MODEL), }); /** diff --git a/server/tests/copilot.test.ts b/server/tests/copilot.test.ts index be3a4847..41d6d4b2 100644 --- a/server/tests/copilot.test.ts +++ b/server/tests/copilot.test.ts @@ -95,11 +95,11 @@ describe("registered Copilot agents", () => { type: "built_in", systemPrompt: "Be helpful.", }, - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, "openai-secret", ), ).toEqual({ - model: "openai/gpt-4.1", + model: "openai/gpt-5.6-terra", // The provenance rule is unconditional, so even a Bot with no tools and no computer carries // it. That Bot needs it most: nothing it says was read anywhere. prompt: `Be helpful.\n\n${PROVENANCE_GUIDANCE}`, @@ -117,7 +117,7 @@ describe("registered Copilot agents", () => { systemPrompt: "Be helpful.", }, ], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, null, ); const agent = agents["general-assistant"]; @@ -159,7 +159,7 @@ describe("registered Copilot agents", () => { endpoint: "http://risk.internal/ag-ui", }, ], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, "openai-secret", ); @@ -200,7 +200,7 @@ describe("registered Copilot agents", () => { endpoint: "http://risk.internal/ag-ui", }, ], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, "openai-secret", stallGuard, ); @@ -227,7 +227,10 @@ describe("registered Copilot agents", () => { endpoint: "http://risk.internal/ag-ui", }, ]; - const model = { provider: "openai" as const, defaultModel: "gpt-4.1" }; + const model = { + provider: "openai" as const, + defaultModel: "gpt-5.6-terra", + }; const guarded = ( await buildAgents(registered, model, null, { @@ -261,12 +264,12 @@ describe("registered Copilot agents", () => { const first = await resolveRuntimeAgents( async () => registered, - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, resolveModelApiKey, ); const second = await resolveRuntimeAgents( async () => registered, - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, resolveModelApiKey, ); @@ -293,7 +296,7 @@ describe("registered Copilot agents", () => { endpoint: "http://risk.internal/ag-ui", }, ], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, async () => { resolverInvoked = true; throw new Error("corrupt model credential"); @@ -339,7 +342,7 @@ describe("standing agent roles", () => { await using endpoint = fakeAgUiEndpoint(); const agents = await buildAgents( [remoteAgent(endpoint.url)], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, null, ); @@ -363,7 +366,7 @@ describe("standing agent roles", () => { await using endpoint = fakeAgUiEndpoint(); const agents = await buildAgents( [remoteAgent(endpoint.url)], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, null, ); @@ -389,7 +392,7 @@ describe("standing agent roles", () => { reason: "Expense Manager has been deleted.", }, ], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, null, ); @@ -417,7 +420,7 @@ describe("standing agent roles", () => { seen.actors.push(actor); return [remoteAgent("http://coworker.internal/ag-ui")]; }, - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, async () => null, ); @@ -436,7 +439,7 @@ describe("standing agent roles", () => { async () => [ remoteAgent("http://coworker.internal/ag-ui", { roleDescription }), ], - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, async () => null, ); const request = new Request("http://openbot.test/api/copilotkit"); @@ -581,7 +584,7 @@ describe("what a Bot is told it holds", () => { type: "built_in", systemPrompt: "Investigate policies.", }, - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, "openai-secret", drive, "BROWSER GUIDANCE HERE", @@ -621,7 +624,7 @@ describe("where a Bot says its answer came from", () => { type: "built_in", systemPrompt: "Be helpful.", }, - { provider: "openai", defaultModel: "gpt-4.1" }, + { provider: "openai", defaultModel: "gpt-5.6-terra" }, "openai-secret", ).prompt as string; diff --git a/server/tests/tenant-package.test.ts b/server/tests/tenant-package.test.ts index f29499dd..40d04198 100644 --- a/server/tests/tenant-package.test.ts +++ b/server/tests/tenant-package.test.ts @@ -80,7 +80,7 @@ function loadedPackage( model: { provider: "openai", credentialSecretRef: "openai-key", - defaultModel: "gpt-4.1", + defaultModel: "gpt-5.6-terra", }, knowledgeSources: [], themeCss: "", @@ -145,7 +145,7 @@ describe("tenant YAML validation", () => { "agents: [{ id: knowledge, name: Knowledge, role_description: Answer company questions., type: built-in, system_prompt: Answer from knowledge. }]", channels: "channels: []", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: "", }), @@ -160,7 +160,7 @@ describe("tenant YAML validation", () => { "agents: [{ id: knowledge, name: Knowledge, title: Company Knowledge, type: built-in, system_prompt: Answer from knowledge. }]", channels: "channels: []", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: "", }), @@ -189,7 +189,7 @@ describe("tenant YAML validation", () => { agents: `agents: [{ id: ${reserved}, name: Knowledge, title: Company Knowledge, role_description: Answer company questions., type: built-in, system_prompt: Answer from knowledge. }]`, channels: "channels: []", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: "", }), @@ -205,7 +205,7 @@ describe("tenant YAML validation", () => { "agents: [{ id: policy-desk, name: Policy Desk, title: Policy, role_description: Answer policy questions., type: built-in, system_prompt: Answer from policy. }]", channels: "channels: []", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: "", }); @@ -233,7 +233,7 @@ describe("tenant YAML validation", () => { endpoint: http://risk.internal/ag-ui`, channels: "channels: []", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: "", }); @@ -250,7 +250,7 @@ describe("tenant YAML validation", () => { "agents: [{ id: knowledge, name: Knowledge, title: Company Knowledge, role_description: Answer company questions., avatar_seed: '', type: built-in, system_prompt: Answer from knowledge. }]", channels: "channels: []", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: "", }), @@ -265,7 +265,7 @@ describe("tenant YAML validation", () => { agents: "agents: []", channels: "channels: []", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: ":root { --primary: oklch(0.32 0.09 250); }", }), @@ -317,7 +317,7 @@ describe("tenant YAML validation", () => { brand: `tenant:\n id: fintech\n product_name: Ledgerline\nskin:\n stylesheet: theme.css`, agents: `agents:\n - id: knowledge\n name: Knowledge\n title: Company Knowledge\n role_description: Answer company questions.\n type: built-in\n system_prompt: Answer from knowledge.\n - id: risk\n name: Risk\n title: Risk & Compliance\n role_description: Investigate policies and controls.\n type: remote-ag-ui\n endpoint: http://risk.internal/ag-ui`, channels: `channels:\n - id: company\n name: Company\n description: Knowledge channel\n permitted_agents: [knowledge, risk]\n allowed_groups: [all]`, - model: `model:\n provider: openai\n credential_secret_ref: openai-key\n default_model: gpt-4.1`, + model: `model:\n provider: openai\n credential_secret_ref: openai-key\n default_model: gpt-5.6-terra`, knowledge: `sources:\n - type: google-drive\n roots: [Policies]`, themeCss: ":root { --primary: black; }", }); @@ -335,7 +335,7 @@ describe("tenant YAML validation", () => { channels: "channels: [{ id: company, name: Company, description: Test, permitted_agents: [missing], allowed_groups: [all] }]", model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: ":root { --primary: black; }", }), @@ -365,7 +365,7 @@ describe("tenant YAML validation", () => { permitted_agents: [knowledge, risk-analyst] allowed_groups: [all]`, model: - "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-5.6-terra }", knowledge: "sources: []", themeCss: "", });