From c00acb36a084b1f0cb32be156c3ee8e1da8684f7 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 4 Sep 2026 10:23:29 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=8C=20feat:=20Add=20GPT-6=20Astra=20Su?= =?UTF-8?q?pport=20(#15567)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐ŸŒŒ feat: Add GPT-6 Astra Support Registers OpenAI's GPT-6 Astra (`gpt-6-astra`) as a first-class model on the canonical OpenAI endpoint. Every model-specific behavior Astra requires โ€” Responses routing for tool calls, stripping the sampling and logprob parameters it rejects, and substituting the `none`/`minimal` reasoning efforts it does not accept โ€” lands in `@librechat/agents` (danny-avila/agents#506) rather than here. None of it is expressible at configuration time: tools bind after `getOpenAIConfig` runs, and LangChain's request builders emit `temperature`/`top_p` from instance fields regardless of what this layer sets. Keeping it in the SDK also means the routing gate can be the documented rule (tools present) instead of the over-approximation the config-time GPT-5.6 path is forced into. What is left here is registration: - 1,050,000-token context window and 128K max output - Model dropdown (config.ts) and the `OPENAI_MODELS` example - Standard pricing: $10/M input, $1/M cached input, $12.50/M cache writes, $50/M output - The >272K long-context tier, which LibreChat already supports through `premiumTokenValues`/`premiumCacheTokenValues`: 2x input and cache rates and 1.5x output for the full request Batch/Flex (50%) and Fast mode (2x) have no representation in the pricing model today and are not introduced here; they apply per-request rather than per-model, so they need a request-shape signal this layer does not yet carry. Tests pin the pricing against the documented multipliers rather than restating the literals โ€” cache writes as 1.25x input, and the premium tier as 2x/1.5x of standard โ€” plus threshold behavior either side of 272K, and that `gpt-6-astra` resolves to its own key for snapshots and provider-prefixed ids instead of collapsing onto a shorter `gpt-6` match. Resolves #15560 Ref: https://developers.openai.com/api/docs/models/gpt-6-astra Ref: https://developers.openai.com/api/docs/guides/latest-model * ๐Ÿงช test: Guard the GPT-6 Astra Max Output Entry [Copilot] The Astra tests asserted the context window but not the 128K max output added alongside it in `anthropicMaxOutputs`' OpenAI counterpart, so that entry could regress silently. The gpt-5.6 tier test above covers both, and the delete-the-fix check showed the gap: removing the source entries failed only the context assertions. Assert `getModelMaxOutputTokens` alongside `getModelMaxTokens`, and fold the snapshot/prefix cases into one loop so both dimensions are covered for every id shape rather than only the bare one. Verified by deleting just the max-output entry: both Astra tests now fail where previously neither did. * ๐Ÿชง feat: Declare the First-Party OpenAI Endpoint to the Agents SDK The agents SDK gates GPT-6 Astra's request constraints โ€” Responses-only tool calls, rejected sampling parameters, unsupported reasoning efforts โ€” on knowing it is talking to the first-party OpenAI surface, and defaults them off. It takes that as a declaration rather than inferring it, because a base URL cannot answer the question: only this layer knows whether a URL is a faithful first-party route, a gateway, or a proxy with its own semantics. That decision already exists here, in `isCanonicalOpenAIBaseURL`, used by the GPT-5.6 Responses default. Declaring it from the same checks keeps it in one place instead of duplicating it downstream where the two could drift โ€” and if the endpoint logic ever moves, it moves once. `firstPartyEndpoint` is declared on `OAIClientOptions` alongside the other agents SDK fields this layer already sets (`promptCache`, `_lc_stream_delay`, `includeReasoningContent`), so it typechecks against the current published SDK and is consumed once the SDK release lands. Two backward-compatibility expectations gain the field. That is the intended signal: they assert the full emitted config precisely so a new field cannot slip through unnoticed. * ๐Ÿ—‚ fix: Keep GPT-6 Astra Out of the Assistants Catalogs [Codex P2] `sharedOpenAIModels` feeds `defaultModels[assistants]` and `[azureAssistants]` as well as the OpenAI and agents catalogs. Astra serves tool calls only from the Responses API, and the Assistants endpoints do not route through `getOpenAILLMConfig`, so listing it there offered a configuration the provider rejects. Astra now sits in its own `responsesOnlyOpenAIModels` list, spread into the OpenAI and agents catalogs only. Agents keeps it deliberately: that path does go through `getOpenAILLMConfig`. * ๐ŸŽš fix: Route GPT-6 Astra to the Responses API at Config Time [Codex P1] `max_tokens` was shaped into `max_completion_tokens` here, from `llmConfig.useResponsesApi`, while the agents SDK switched Astra to the Responses API later at invocation time โ€” so the request reached an endpoint expecting `max_output_tokens` and was rejected. Deciding the API here fixes that, and generalizes: any field whose shape depends on the endpoint has the same problem, so the choice has to precede the shaping. It also lands before Azure replaces `llmConfig.model` with a deployment name, which is the only point that still knows the real model id. Unlike the GPT-5.6 rule above, this does not depend on reasoning parameters. Astra serves tool calls only from Responses, and OpenAI recommends Responses for it generally, so a static decision loses nothing โ€” which was the reason the SDK was deciding it dynamically. The existing guards carry over: OpenRouter, custom gateways, and an explicit `dropParams` opt-out keep their configured path. The agents SDK is still required, for the request shaping Astra needs on either API (danny-avila/agents#506); it no longer decides routing. * ๐Ÿท fix: Record the Served Model Before Azure Overwrites It [Codex P1] `llmConfig.model` is unconditionally replaced with the Azure deployment name, so the agents SDK received an alias and could not tell which model it was talking to โ€” a deployment not literally named `gpt-6-astra` got none of the request shaping Astra requires. Records the real id in `servedModel` immediately before the overwrite, which is the last point that still knows it. The SDK prefers it over `model` when deciding which model-specific constraints apply (danny-avila/agents#506). Set for every first-party Azure request rather than only Astra: "what this deployment serves" is a fact this layer knows and the SDK decides what to do with, and keeping model-specific knowledge out of here is the point of the split. The backward-compatibility expectation gains the field, which is that test doing its job. * ๐ŸŽ› fix: Do Not Let a reasoning_effort Drop Disable Astra Routing [Codex P1] The Astra routing reused the GPT-5.6 opt-out wholesale, which treats `dropParams: ['reasoning_effort']` as opting out of the Responses API. That holds for the 5.6 default, which routes *because* of reasoning โ€” removing the effort removes its reason to route. Astra's routing is not reasoning-driven, so a rule clearing an unsupported stored effort silently put it back on Chat Completions, where its tool calls fail. Astra now honors only an explicit `useResponsesApi` drop. Reusing the guard without re-deriving whether each clause applied was the error. * โœ‚๏ธ fix: Scope GPT-6 Astra to the First-Party OpenAI Endpoint [Codex P1 + P2] Two findings, both Azure, resolved by removing the Azure support rather than extending it. Azure's first-party hosts do not satisfy `isCanonicalOpenAIBaseURL`, so a configured `*.openai.azure.com` was neither routed to Responses nor declared first-party; and moving Astra out of `sharedOpenAIModels` also removed it from the Azure chat fallback, since `getOpenAIModels({ azure: true })` resolves through `defaultModels[azureAssistants]`. Both are moot once the scope is right: OpenAI does not document Astra as available on Azure OpenAI. Its model page lists the API and OpenAI's own subscription plans, with no third-party cloud surface. The Azure handling was support for a deployment the model is not documented to reach, and it produced most of the review findings on this branch and its agents SDK counterpart. Routing and the first-party declaration are now scoped to `EModelEndpoint.openAI`, and the Azure-only `servedModel` recording is gone along with its SDK counterpart. Astra staying out of the Azure catalogs is now the intended outcome rather than a regression. * ๐Ÿšซ fix: Keep Astra Out of the Initial Azure Catalog [Codex P1] `initialModelsConfig` feeds the OpenAI catalog to Azure OpenAI as well, so scoping Astra to the first-party OpenAI endpoint left it advertised on Azure anyway โ€” and being first in the list, it could become the default selection where no prior choice exists. The server's Azure fallback already excludes it, so the client would offer a model the server then rejects. The Azure initial catalog now uses the OpenAI list minus the Responses-only models, following the same scoping decision the routing guard makes. The initial Assistants catalog was already safe: `fitlerAssistantModels` requires `gpt-4` or `gpt-3.5`, which Astra does not match. Pinned that in a test rather than leaving it to a regex written for another purpose. * โฌ†๏ธ chore: Bump `@librechat/agents` to 3.7.20 for GPT-6 Astra Constraints --- .env.example | 2 +- api/package.json | 2 +- package-lock.json | 10 +- packages/api/package.json | 2 +- .../openai/config.backward-compat.spec.ts | 1 + packages/api/src/endpoints/openai/llm.spec.ts | 116 ++++++++++++++++++ packages/api/src/endpoints/openai/llm.ts | 58 +++++++++ packages/api/src/types/openai.ts | 9 ++ packages/api/src/utils/tokens.spec.ts | 18 +++ packages/api/src/utils/tokens.ts | 2 + .../data-provider/specs/astra-catalog.spec.ts | 45 +++++++ packages/data-provider/src/config.ts | 25 +++- packages/data-schemas/src/methods/tx.spec.ts | 53 ++++++++ packages/data-schemas/src/methods/tx.ts | 4 + 14 files changed, 337 insertions(+), 10 deletions(-) create mode 100644 packages/data-provider/specs/astra-catalog.spec.ts diff --git a/.env.example b/.env.example index e726d42112..5f637ee116 100644 --- a/.env.example +++ b/.env.example @@ -587,7 +587,7 @@ GOOGLE_KEY=user_provided #============# OPENAI_API_KEY=user_provided -# OPENAI_MODELS=gpt-5.6,gpt-5.6-terra,gpt-5.6-luna,gpt-5.5,gpt-5.5-pro,chat-latest,gpt-5.4,gpt-5.4-pro,gpt-5.4-mini,gpt-5.4-nano,gpt-5.3-codex,gpt-5.2,gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,o3-pro,o3,o4-mini,gpt-4.1,gpt-4.1-mini,gpt-4.1-nano,o3-mini,o1-pro,o1,gpt-4o,gpt-4o-mini +# OPENAI_MODELS=gpt-6-astra,gpt-5.6,gpt-5.6-terra,gpt-5.6-luna,gpt-5.5,gpt-5.5-pro,chat-latest,gpt-5.4,gpt-5.4-pro,gpt-5.4-mini,gpt-5.4-nano,gpt-5.3-codex,gpt-5.2,gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,o3-pro,o3,o4-mini,gpt-4.1,gpt-4.1-mini,gpt-4.1-nano,o3-mini,o1-pro,o1,gpt-4o,gpt-4o-mini DEBUG_OPENAI=false diff --git a/api/package.json b/api/package.json index d015228905..71959412f2 100644 --- a/api/package.json +++ b/api/package.json @@ -46,7 +46,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^2.8.0", "@keyv/redis": "5.1.6", - "@librechat/agents": "^3.7.19", + "@librechat/agents": "^3.7.20", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index f411f9d288..12bb93691e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^2.8.0", "@keyv/redis": "5.1.6", - "@librechat/agents": "^3.7.19", + "@librechat/agents": "^3.7.20", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -10627,9 +10627,9 @@ } }, "node_modules/@librechat/agents": { - "version": "3.7.19", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.7.19.tgz", - "integrity": "sha512-/dHHKBvLoXjX1hFkDJBGP5CjkMp1vS+e3OyLqffa4c7JCenFhOHThBDUdZmCDvoEylRhima0h3fYl/JqCR8+hQ==", + "version": "3.7.20", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.7.20.tgz", + "integrity": "sha512-YnfBpC5MK7QDgP+6VpmOYBTA2mXbaA+fDggfi7n/bsB56FSqenCzizSpC+Nn83lhwJz7GNntJpYNX53nRhFGgw==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.115.0", @@ -42681,7 +42681,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^2.8.0", "@keyv/redis": "5.1.6", - "@librechat/agents": "^3.7.19", + "@librechat/agents": "^3.7.20", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.30.0", "@opentelemetry/api": "^1.9.0", diff --git a/packages/api/package.json b/packages/api/package.json index d8193a0e3c..66f436c601 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -113,7 +113,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^2.8.0", "@keyv/redis": "5.1.6", - "@librechat/agents": "^3.7.19", + "@librechat/agents": "^3.7.20", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.30.0", "@opentelemetry/api": "^1.9.0", diff --git a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts index acc3496b2b..f89482f534 100644 --- a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts +++ b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts @@ -33,6 +33,7 @@ describe('getOpenAIConfig - Backward Compatibility', () => { streaming: true, model: 'gpt-5-nano', useResponsesApi: true, + firstPartyEndpoint: true, user: 'some-user', apiKey: 'sk-proj-somekey', reasoning: { diff --git a/packages/api/src/endpoints/openai/llm.spec.ts b/packages/api/src/endpoints/openai/llm.spec.ts index 23db811e1d..faa5edc25c 100644 --- a/packages/api/src/endpoints/openai/llm.spec.ts +++ b/packages/api/src/endpoints/openai/llm.spec.ts @@ -709,6 +709,122 @@ describe('getOpenAILLMConfig', () => { }); }); + describe('GPT-6 Astra Responses API routing', () => { + const astraConfig = (overrides: Record = {}) => + getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + endpoint: EModelEndpoint.openAI, + modelOptions: { model: 'gpt-6-astra' }, + ...overrides, + }); + + it('routes every Astra turn to the Responses API, not only reasoning ones', () => { + expect(astraConfig().llmConfig).toHaveProperty('useResponsesApi', true); + }); + + /** + * The reason routing is decided here rather than at invocation time: the + * max-tokens field is shaped from `useResponsesApi`, so switching APIs later + * would send `max_completion_tokens` to an endpoint expecting + * `max_output_tokens`. + */ + it('shapes max tokens for the API it actually uses', () => { + const result = astraConfig({ + modelOptions: { model: 'gpt-6-astra', max_tokens: 4096 }, + }); + const kwargs = (result.llmConfig.modelKwargs ?? {}) as Record; + expect(kwargs).not.toHaveProperty('max_completion_tokens'); + expect(kwargs.max_output_tokens ?? result.llmConfig.maxTokens).toBeDefined(); + }); + + it('keeps routing when a drop rule only clears reasoning_effort', () => { + /** + * Unlike the GPT-5.6 default, Astra's routing is not reasoning-driven, so + * a rule clearing an unsupported stored effort must not disable it. + */ + expect(astraConfig({ dropParams: ['reasoning_effort'] }).llmConfig).toHaveProperty( + 'useResponsesApi', + true, + ); + }); + + it('respects an explicit opt-out', () => { + expect(astraConfig({ dropParams: ['useResponsesApi'] }).llmConfig).not.toHaveProperty( + 'useResponsesApi', + true, + ); + }); + + it('leaves a custom gateway on its configured path', () => { + expect(astraConfig({ baseURL: 'https://gateway.internal/v1' }).llmConfig).not.toHaveProperty( + 'useResponsesApi', + true, + ); + }); + + it('does not change routing for other models', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + endpoint: EModelEndpoint.openAI, + modelOptions: { model: 'gpt-5.5' }, + }); + expect(result.llmConfig).not.toHaveProperty('useResponsesApi', true); + }); + }); + + describe('First-party endpoint declaration', () => { + /** + * The agents SDK gates its model-specific request constraints on this flag + * and defaults them off, rather than inferring the endpoint from a base + * URL. Only this layer can tell a faithful first-party route from a + * gateway, so the decision is made here and declared downstream. + */ + const configFor = (overrides: Record = {}) => + getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + endpoint: EModelEndpoint.openAI, + modelOptions: { model: 'gpt-6-astra' }, + ...overrides, + }); + + it('declares the first-party endpoint for canonical OpenAI', () => { + expect(configFor().llmConfig).toHaveProperty('firstPartyEndpoint', true); + }); + + it('declares it for an explicit api.openai.com base URL', () => { + expect(configFor({ baseURL: 'https://api.openai.com/v1' }).llmConfig).toHaveProperty( + 'firstPartyEndpoint', + true, + ); + }); + + /** + * Astra is not documented as available on Azure OpenAI, and Azure's + * first-party hosts do not satisfy the OpenAI-host check, so declaring it + * there would claim a surface this cannot verify. + */ + it('does not declare it for Azure OpenAI', () => { + expect(configFor({ endpoint: EModelEndpoint.azureOpenAI }).llmConfig).not.toHaveProperty( + 'firstPartyEndpoint', + ); + }); + + it('does not declare it for a custom gateway base URL', () => { + expect(configFor({ baseURL: 'https://gateway.internal/v1' }).llmConfig).not.toHaveProperty( + 'firstPartyEndpoint', + ); + }); + + it('does not declare it for a non-OpenAI endpoint', () => { + expect(configFor({ endpoint: EModelEndpoint.custom }).llmConfig).not.toHaveProperty( + 'firstPartyEndpoint', + ); + }); + }); + describe('GPT-5.6 Responses API Requirement', () => { it.each(['gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.6-sol', 'gpt-5.6'])( 'should default to Responses API for %s when reasoning_effort is set', diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index 282aca2a17..04f02e1810 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -130,6 +130,24 @@ function isOpenAIEndpoint(endpoint?: EModelEndpoint | string | null): boolean { */ const responsesApiRequiredPattern = /\bgpt-5\.6\b/; +/** + * Models that take the Responses API for every turn, not only reasoning ones. + * OpenAI's guidance for GPT-6 Astra is to use Responses, and tool calls require + * it outright. + * + * Decided here rather than in the agents SDK at invocation time: the max-tokens + * field below is shaped from `useResponsesApi`, so a later switch would send + * `max_completion_tokens` to an endpoint expecting `max_output_tokens`. Config + * time is also the only place that knows the model before Azure replaces it + * with a deployment name. + * @see https://developers.openai.com/api/docs/guides/latest-model + */ +const responsesApiPreferredPattern = /^gpt-6-astra(?:-|$)/i; + +function prefersResponsesApi(model?: string): boolean { + return typeof model === 'string' && responsesApiPreferredPattern.test(model); +} + function requiresResponsesApiForReasoning({ model, reasoningEffort, @@ -848,6 +866,14 @@ export function getOpenAILLMConfig({ const responsesApiOptedOut = dropParams != null && (dropParams.includes('reasoning_effort') || dropParams.includes('useResponsesApi')); + /** + * The GPT-5.6 default above is reasoning-driven, so dropping `reasoning_effort` + * removes its reason to route. Astra's is not: it takes Responses for every + * turn, and a drop rule clearing an unsupported stored effort must not also + * disable its routing. Only an explicit `useResponsesApi` drop does that. + */ + const responsesApiExplicitlyOptedOut = + dropParams != null && dropParams.includes('useResponsesApi'); if ( !useOpenRouter && endpoint === EModelEndpoint.openAI && @@ -860,6 +886,38 @@ export function getOpenAILLMConfig({ llmConfig.useResponsesApi = true; } + /** + * Route GPT-6 Astra to the Responses API for every turn. Unlike the GPT-5.6 + * rule above this does not depend on reasoning params: Astra serves tool calls + * only from Responses, and OpenAI recommends it generally. + */ + if ( + !useOpenRouter && + endpoint === EModelEndpoint.openAI && + isCanonicalOpenAIBaseURL(baseURL) && + llmConfig.useResponsesApi == null && + !responsesApiExplicitlyOptedOut && + prefersResponsesApi(llmConfig.model) + ) { + llmConfig.useResponsesApi = true; + } + + /** + * Declare the first-party surface for the agents SDK's model-specific request + * constraints. Computed here, from the same checks the Responses default + * above uses, so the decision lives in one place: OpenRouter and custom + * gateways route through endpoints whose contract is not OpenAI's, and only + * this layer can tell them apart. + * + * Scoped to the canonical OpenAI endpoint. Astra is not documented as + * available on Azure OpenAI, and Azure's first-party hosts do not satisfy the + * OpenAI-host check, so declaring it there would claim a surface this cannot + * verify. + */ + if (!useOpenRouter && endpoint === EModelEndpoint.openAI && isCanonicalOpenAIBaseURL(baseURL)) { + llmConfig.firstPartyEndpoint = true; + } + if (!useOpenRouter) { hasModelKwargs = applyReasoningConfig({ diff --git a/packages/api/src/types/openai.ts b/packages/api/src/types/openai.ts index e0c7923ed7..8e05b0711b 100644 --- a/packages/api/src/types/openai.ts +++ b/packages/api/src/types/openai.ts @@ -36,6 +36,15 @@ export type OAIClientOptions = Omit & { includeReasoningContent?: boolean; promptCache?: boolean; promptCacheTtl?: '5m' | '1h'; + /** + * Declares that this client talks to the first-party OpenAI surface, which is + * what gates the agents SDK's model-specific request constraints (GPT-6 + * Astra: Responses-only tool calls, rejected sampling parameters, + * unsupported reasoning efforts). The SDK defaults them off and takes this as + * a declaration rather than inferring it from a base URL, because only this + * layer knows whether a URL is a faithful first-party route or a gateway. + */ + firstPartyEndpoint?: boolean; _lc_stream_delay?: number; verbosity?: string | null; }; diff --git a/packages/api/src/utils/tokens.spec.ts b/packages/api/src/utils/tokens.spec.ts index aac009bce3..8d9070e149 100644 --- a/packages/api/src/utils/tokens.spec.ts +++ b/packages/api/src/utils/tokens.spec.ts @@ -91,6 +91,24 @@ describe('Gemini 3.8 Flash', () => { }); }); +describe('GPT-6 Astra', () => { + it('resolves 1.05M context and 128K output', () => { + expect(getModelMaxTokens('gpt-6-astra', EModelEndpoint.openAI)).toBe(1050000); + expect(getModelMaxOutputTokens('gpt-6-astra', EModelEndpoint.openAI)).toBe(128000); + }); + + it('matches its own key for snapshots and provider prefixes', () => { + for (const model of [ + 'gpt-6-astra-2026-04-30', + 'openai/gpt-6-astra', + 'gpt-6-astra-2026-04-30/openai', + ]) { + expect(getModelMaxTokens(model, EModelEndpoint.openAI)).toBe(1050000); + expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(128000); + } + }); +}); + describe('Qwen3.5 and later generations', () => { it('resolves 262K for the vLLM model id that regressed to the qwen3 window', () => { expect(getModelMaxTokens('Qwen/Qwen3.5-397B-A17B-FP8', EModelEndpoint.openAI)).toBe(262144); diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 951f44a020..38048affb6 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -64,6 +64,7 @@ const openAIModels = { 'gpt-5.6': 1050000, 'gpt-5.6-terra': 1050000, 'gpt-5.6-luna': 1050000, + 'gpt-6-astra': 1050000, // >272K input prices at the long-context tier (2x input/cache, 1.5x output) 'chat-latest': 400000, 'gpt-5-mini': 400000, 'gpt-5-nano': 400000, @@ -508,6 +509,7 @@ export const modelMaxOutputs = { 'gpt-5.6': 128000, 'gpt-5.6-terra': 128000, 'gpt-5.6-luna': 128000, + 'gpt-6-astra': 128000, 'chat-latest': 128000, 'gpt-5-mini': 128000, 'gpt-5-nano': 128000, diff --git a/packages/data-provider/specs/astra-catalog.spec.ts b/packages/data-provider/specs/astra-catalog.spec.ts new file mode 100644 index 0000000000..3b0d6580ce --- /dev/null +++ b/packages/data-provider/specs/astra-catalog.spec.ts @@ -0,0 +1,45 @@ +import { EModelEndpoint } from '../src/schemas'; +import { defaultModels, initialModelsConfig } from '../src/config'; + +/** + * GPT-6 Astra serves tool calls only from the Responses API. The Assistants + * endpoints do not route through `getOpenAILLMConfig`, so listing it there + * would offer a configuration the provider rejects. + */ +describe('GPT-6 Astra catalog placement', () => { + it('is offered on the endpoints that route through the OpenAI config', () => { + expect(defaultModels[EModelEndpoint.openAI]).toContain('gpt-6-astra'); + expect(defaultModels[EModelEndpoint.agents]).toContain('gpt-6-astra'); + }); + + it('is kept out of both Assistants catalogs', () => { + expect(defaultModels[EModelEndpoint.assistants]).not.toContain('gpt-6-astra'); + expect(defaultModels[EModelEndpoint.azureAssistants]).not.toContain('gpt-6-astra'); + }); + + it('is kept out of the initial Azure and Assistants catalogs', () => { + /** + * Azure shares the OpenAI list, but Astra gets neither Responses routing nor + * its request constraints there โ€” and being first in the list would let it + * become the default selection. + */ + expect(initialModelsConfig[EModelEndpoint.azureOpenAI]).not.toContain('gpt-6-astra'); + expect(initialModelsConfig[EModelEndpoint.assistants]).not.toContain('gpt-6-astra'); + }); + + it('keeps Astra in the initial catalogs that can run it', () => { + expect(initialModelsConfig[EModelEndpoint.openAI]).toContain('gpt-6-astra'); + expect(initialModelsConfig[EModelEndpoint.agents]).toContain('gpt-6-astra'); + }); + + it('does not disturb the rest of the shared OpenAI catalog', () => { + for (const endpoint of [ + EModelEndpoint.openAI, + EModelEndpoint.agents, + EModelEndpoint.assistants, + EModelEndpoint.azureAssistants, + ]) { + expect(defaultModels[endpoint]).toContain('gpt-5.6'); + } + }); +}); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 7cbea42d19..852293df94 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -2695,6 +2695,15 @@ export const alternateName = { [KnownEndpoints.helicone]: 'Helicone', }; +/** + * Models the Assistants endpoints cannot run. GPT-6 Astra serves tool calls only + * from the Responses API, and the Assistants surface does not route through + * `getOpenAILLMConfig`, so listing it there would offer a configuration the + * provider rejects. Kept out of `sharedOpenAIModels`, which both Assistants + * catalogs consume. + */ +const responsesOnlyOpenAIModels = ['gpt-6-astra']; + const sharedOpenAIModels = [ 'gpt-5.6', 'gpt-5.6-terra', @@ -2797,7 +2806,8 @@ export const bedrockModels = [ export const defaultModels = { [EModelEndpoint.azureAssistants]: sharedOpenAIModels, [EModelEndpoint.assistants]: [...sharedOpenAIModels, 'chatgpt-4o-latest'], - [EModelEndpoint.agents]: sharedOpenAIModels, // TODO: Add agent models (agentsModels) + // TODO: Add agent models (agentsModels) + [EModelEndpoint.agents]: [...responsesOnlyOpenAIModels, ...sharedOpenAIModels], [EModelEndpoint.google]: [ // Gemini 3.8 Models 'gemini-3.8-flash', @@ -2822,6 +2832,7 @@ export const defaultModels = { ], [EModelEndpoint.anthropic]: sharedAnthropicModels, [EModelEndpoint.openAI]: [ + ...responsesOnlyOpenAIModels, ...sharedOpenAIModels, 'chatgpt-4o-latest', 'gpt-4-vision-preview', @@ -2837,12 +2848,22 @@ const fitlerAssistantModels = (str: string) => { const openAIModels = defaultModels[EModelEndpoint.openAI]; +/** + * The OpenAI catalog without the models only the first-party OpenAI endpoint + * can run. Azure OpenAI shares this list, but Astra is neither routed to the + * Responses API nor given its request constraints there, and listing it first + * would let it become the default selection. + */ +const nonResponsesOnlyOpenAIModels = openAIModels.filter( + (model) => !responsesOnlyOpenAIModels.includes(model), +); + export const initialModelsConfig: TModelsConfig = { initial: [], [EModelEndpoint.openAI]: openAIModels, [EModelEndpoint.assistants]: openAIModels.filter(fitlerAssistantModels), [EModelEndpoint.agents]: openAIModels, // TODO: Add agent models (agentsModels) - [EModelEndpoint.azureOpenAI]: openAIModels, + [EModelEndpoint.azureOpenAI]: nonResponsesOnlyOpenAIModels, [EModelEndpoint.google]: defaultModels[EModelEndpoint.google], [EModelEndpoint.anthropic]: defaultModels[EModelEndpoint.anthropic], [EModelEndpoint.bedrock]: defaultModels[EModelEndpoint.bedrock], diff --git a/packages/data-schemas/src/methods/tx.spec.ts b/packages/data-schemas/src/methods/tx.spec.ts index 676484c963..0630ba57ab 100644 --- a/packages/data-schemas/src/methods/tx.spec.ts +++ b/packages/data-schemas/src/methods/tx.spec.ts @@ -470,6 +470,59 @@ describe('getMultiplier', () => { } }); + it('should use the documented gpt-6-astra pricing', () => { + expect(tokenValues['gpt-6-astra']).toEqual({ prompt: 10, completion: 50 }); + expect(cacheTokenValues['gpt-6-astra']).toEqual({ write: 12.5, read: 1 }); + expect(premiumTokenValues['gpt-6-astra']).toEqual({ + threshold: 272000, + prompt: 20, + completion: 75, + }); + expect(premiumCacheTokenValues['gpt-6-astra']).toEqual({ + threshold: 272000, + write: 25, + read: 2, + }); + }); + + it('should bill gpt-6-astra cache writes at the documented 1.25x input surcharge', () => { + expect(cacheTokenValues['gpt-6-astra'].write).toBeCloseTo( + tokenValues['gpt-6-astra'].prompt * 1.25, + ); + }); + + it('should apply the documented gpt-6-astra long-context multipliers', () => { + const standard = tokenValues['gpt-6-astra']; + const premium = premiumTokenValues['gpt-6-astra']; + const standardCache = cacheTokenValues['gpt-6-astra']; + const premiumCache = premiumCacheTokenValues['gpt-6-astra']; + /** >272K input: 2x input and cache rates, 1.5x output, for the full request. */ + expect(premium.prompt).toBeCloseTo(standard.prompt * 2); + expect(premium.completion).toBeCloseTo(standard.completion * 1.5); + expect(premiumCache.write).toBeCloseTo(standardCache.write * 2); + expect(premiumCache.read).toBeCloseTo(standardCache.read * 2); + }); + + it('should resolve gpt-6-astra to its own key rather than a gpt-6 prefix', () => { + for (const model of [ + 'gpt-6-astra', + 'gpt-6-astra-2026-04-30', + 'openai/gpt-6-astra', + 'gpt-6-astra/openai', + ]) { + expect(getValueKey(model)).toBe('gpt-6-astra'); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(10); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe(50); + } + }); + + it('should charge gpt-6-astra premium rates only past the 272K threshold', () => { + const model = 'gpt-6-astra'; + expect(getMultiplier({ model, tokenType: 'prompt', inputTokenCount: 272000 })).toBe(10); + expect(getMultiplier({ model, tokenType: 'prompt', inputTokenCount: 272001 })).toBe(20); + expect(getMultiplier({ model, tokenType: 'completion', inputTokenCount: 272001 })).toBe(75); + }); + it('should use the documented gpt-5.6 pricing', () => { const expectedPricing = { 'gpt-5.6': { diff --git a/packages/data-schemas/src/methods/tx.ts b/packages/data-schemas/src/methods/tx.ts index d2e3548807..acc9e2d001 100644 --- a/packages/data-schemas/src/methods/tx.ts +++ b/packages/data-schemas/src/methods/tx.ts @@ -150,6 +150,7 @@ export const tokenValues: Record 'gpt-5.6': { prompt: 4, completion: 20 }, 'gpt-5.6-terra': { prompt: 2, completion: 12 }, 'gpt-5.6-luna': { prompt: 0.2, completion: 1.2 }, + 'gpt-6-astra': { prompt: 10, completion: 50 }, 'chat-latest': { prompt: 5, completion: 30 }, 'gpt-5-chat-latest': { prompt: 1.25, completion: 10 }, 'gpt-5.1-chat-latest': { prompt: 1.25, completion: 10 }, @@ -389,6 +390,7 @@ export const cacheTokenValues: Record = 'gpt-5.6': { write: 5, read: 0.4 }, 'gpt-5.6-terra': { write: 2.5, read: 0.2 }, 'gpt-5.6-luna': { write: 0.25, read: 0.02 }, + 'gpt-6-astra': { write: 12.5, read: 1 }, 'chat-latest': { write: 5, read: 0.5 }, 'gpt-5-chat-latest': { write: 1.25, read: 0.125 }, 'gpt-5.1-chat-latest': { write: 1.25, read: 0.125 }, @@ -445,6 +447,7 @@ export const premiumTokenValues: Record< 'gpt-5.6': { threshold: 272000, prompt: 8, completion: 30 }, 'gpt-5.6-terra': { threshold: 272000, prompt: 4, completion: 18 }, 'gpt-5.6-luna': { threshold: 272000, prompt: 0.4, completion: 1.8 }, + 'gpt-6-astra': { threshold: 272000, prompt: 20, completion: 75 }, 'grok-4.5': { threshold: 200000, prompt: 4, completion: 12 }, 'grok-4-5': { threshold: 200000, prompt: 4, completion: 12 }, 'grok-4.6': { threshold: 200000, prompt: 4, completion: 12 }, @@ -466,6 +469,7 @@ export const premiumCacheTokenValues: Record< 'gpt-5.6': { threshold: 272000, write: 10, read: 0.8 }, 'gpt-5.6-terra': { threshold: 272000, write: 5, read: 0.4 }, 'gpt-5.6-luna': { threshold: 272000, write: 0.5, read: 0.04 }, + 'gpt-6-astra': { threshold: 272000, write: 25, read: 2 }, }; export function createTxMethods(