🌌 feat: Add GPT-6 Astra Support (#15567)

* 🌌 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
This commit is contained in:
Danny Avila 2026-09-04 10:23:29 -04:00 • committed by GitHub
parent 9a64e85b1e
commit c00acb36a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 337 additions and 10 deletions

View file

@ -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

View file

@ -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",

10
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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: {

View file

@ -709,6 +709,122 @@ describe('getOpenAILLMConfig', () => {
});
});
describe('GPT-6 Astra Responses API routing', () => {
const astraConfig = (overrides: Record<string, unknown> = {}) =>
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<string, unknown>;
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<string, unknown> = {}) =>
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',

View file

@ -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({

View file

@ -36,6 +36,15 @@ export type OAIClientOptions = Omit<OpenAIClientOptions, 'verbosity'> & {
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;
};

View file

@ -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);

View file

@ -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,

View file

@ -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');
}
});
});

View file

@ -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],

View file

@ -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': {

View file

@ -150,6 +150,7 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'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<string, { write: number; read: number }> =
'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(