mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-04 04:53:39 +00:00
feat(ai): add GPT-6.1 Terra to the OpenAI catalog
This commit is contained in:
1 parent
f850155411
commit
0322d3aa2c
6 files changed
+111
-64
No files matched your search
+26
-22
@@ -177,6 +177,7 @@ describe('OpenAiChatProvider model catalog', () => {
|
||||
expect(ids).toContain('gpt-5.2-2025-12-11');
|
||||
expect(ids).toContain('gpt-6-astra');
|
||||
expect(ids).toContain('gpt-6.1-sol');
|
||||
expect(ids).toContain('gpt-6.1-terra');
|
||||
expect(ids).toContain('gpt-6-sol');
|
||||
expect(ids).toContain('gpt-6-luna');
|
||||
});
|
||||
@@ -348,30 +349,33 @@ describe('OpenAiChatProvider.complete request shape', () => {
|
||||
expect(args.safety_identifier).toBe('puter-u42');
|
||||
});
|
||||
|
||||
it.each(['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-luna'])(
|
||||
'resolves the namespaced %s alias',
|
||||
async (model) => {
|
||||
const { provider } = makeProvider();
|
||||
createMock.mockResolvedValueOnce(baseCompletion);
|
||||
it.each([
|
||||
'gpt-6-astra',
|
||||
'gpt-6.1-sol',
|
||||
'gpt-6.1-terra',
|
||||
'gpt-6-sol',
|
||||
'gpt-6-luna',
|
||||
])('resolves the namespaced %s alias', async (model) => {
|
||||
const { provider } = makeProvider();
|
||||
createMock.mockResolvedValueOnce(baseCompletion);
|
||||
|
||||
await withTestActor(() =>
|
||||
provider.complete({
|
||||
model: `openai/${model}`,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
reasoning_effort: 'low',
|
||||
}),
|
||||
);
|
||||
await withTestActor(() =>
|
||||
provider.complete({
|
||||
model: `openai/${model}`,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
reasoning_effort: 'low',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(createMock.mock.calls[0]![0].model).toBe(model);
|
||||
expect(createMock.mock.calls[0]![0].reasoning_effort).toBe('low');
|
||||
expect(recordSpy).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.anything(),
|
||||
`openai:${model}`,
|
||||
expect.any(Object),
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(createMock.mock.calls[0]![0].model).toBe(model);
|
||||
expect(createMock.mock.calls[0]![0].reasoning_effort).toBe('low');
|
||||
expect(recordSpy).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.anything(),
|
||||
`openai:${model}`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it('omits parallel_tool_calls/tool_choice when no tool is sent (Chat Completions rejects them alone)', async () => {
|
||||
const { provider } = makeProvider();
|
||||
|
||||
@@ -224,6 +224,7 @@ describe('OpenAiResponsesChatProvider model catalog', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
['gpt-6.1-terra', '2026-06-01', '2026-10-26', 100, 5, 500],
|
||||
['gpt-6.1-sol', '2026-04-30', '2026-09-29', 200, 10, 1000],
|
||||
['gpt-6-sol', '2026-04-20', '2026-09-22', 200, 20, 1000],
|
||||
['gpt-6-luna', '2026-05-18', '2026-09-22', 10, 1, 50],
|
||||
@@ -244,6 +245,7 @@ describe('OpenAiResponsesChatProvider model catalog', () => {
|
||||
tokens: 1_000_000,
|
||||
prompt_tokens: input,
|
||||
cached_tokens: cached,
|
||||
cache_write_tokens: input * 1.25,
|
||||
completion_tokens: output,
|
||||
},
|
||||
context: 1_050_000,
|
||||
@@ -332,6 +334,7 @@ describe('OpenAiResponsesChatProvider.complete request shape', () => {
|
||||
'gpt-6-luna',
|
||||
'gpt-6-astra',
|
||||
'gpt-6.1-sol',
|
||||
'gpt-6.1-terra',
|
||||
'gpt-5.6',
|
||||
'openai/gpt-5.6',
|
||||
'openai/gpt-5.6-luna',
|
||||
@@ -367,7 +370,7 @@ describe('OpenAiResponsesChatProvider.complete request shape', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['gpt-5.6-luna', 'gpt-6-luna'])(
|
||||
it.each(['gpt-5.6-luna', 'gpt-6-luna', 'gpt-6.1-terra'])(
|
||||
'preserves sampling with reasoning disabled for %s',
|
||||
async (model) => {
|
||||
const { provider } = makeProvider();
|
||||
@@ -620,7 +623,13 @@ describe('OpenAiResponsesChatProvider.complete request shape', () => {
|
||||
expect(gpt41Args).not.toHaveProperty('verbosity');
|
||||
});
|
||||
|
||||
it.each(['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-luna'])(
|
||||
it.each([
|
||||
'gpt-6-astra',
|
||||
'gpt-6.1-sol',
|
||||
'gpt-6.1-terra',
|
||||
'gpt-6-sol',
|
||||
'gpt-6-luna',
|
||||
])(
|
||||
'maps flat controls to Responses options for %s aliases',
|
||||
async (model) => {
|
||||
const { provider } = makeProvider();
|
||||
@@ -860,47 +869,51 @@ describe('OpenAiResponsesChatProvider.complete non-stream output', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('bills the whole request at long-context rates past 272K input tokens', async () => {
|
||||
const sol = OPEN_AI_MODELS.find((m) => m.id === 'gpt-6-sol')!;
|
||||
const { provider } = makeProvider();
|
||||
responsesCreateMock.mockResolvedValueOnce({
|
||||
output: [{ role: 'assistant' }],
|
||||
output_text: 'hi',
|
||||
usage: {
|
||||
input_tokens: 300_000,
|
||||
output_tokens: 10_000,
|
||||
input_tokens_details: {
|
||||
cached_tokens: 50_000,
|
||||
cache_write_tokens: 20_000,
|
||||
it.each([
|
||||
['gpt-6-sol', 119],
|
||||
['gpt-6.1-terra', 59],
|
||||
] as const)(
|
||||
'bills %s at long-context rates past 272K input tokens',
|
||||
async (modelId, total) => {
|
||||
const model = OPEN_AI_MODELS.find((m) => m.id === modelId)!;
|
||||
const { provider } = makeProvider();
|
||||
responsesCreateMock.mockResolvedValueOnce({
|
||||
output: [{ role: 'assistant' }],
|
||||
output_text: 'hi',
|
||||
usage: {
|
||||
input_tokens: 300_000,
|
||||
output_tokens: 10_000,
|
||||
input_tokens_details: {
|
||||
cached_tokens: 50_000,
|
||||
cache_write_tokens: 20_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await withTestActor(() =>
|
||||
provider.complete({
|
||||
model: 'gpt-6-sol',
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
}),
|
||||
);
|
||||
await withTestActor(() =>
|
||||
provider.complete({
|
||||
model: modelId,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
}),
|
||||
);
|
||||
|
||||
const [, , , overrides] = recordSpy.mock.calls[0]!;
|
||||
// $0.92 + $0.02 + $0.10 + $0.15 = $1.19, against $0.62 at the
|
||||
// standard rates.
|
||||
expect(overrides).toEqual({
|
||||
prompt_tokens: 230_000 * Number(sol.costs.prompt_tokens) * 2,
|
||||
cached_tokens: 50_000 * Number(sol.costs.cached_tokens) * 2,
|
||||
cache_write_tokens:
|
||||
20_000 * Number(sol.costs.cache_write_tokens) * 2,
|
||||
completion_tokens:
|
||||
10_000 * Number(sol.costs.completion_tokens) * 1.5,
|
||||
});
|
||||
const totalCents =
|
||||
Object.values(overrides as Record<string, number>).reduce(
|
||||
(a, b) => a + b,
|
||||
0,
|
||||
) / 1_000_000;
|
||||
expect(totalCents).toBeCloseTo(119);
|
||||
});
|
||||
const [, , , overrides] = recordSpy.mock.calls[0]!;
|
||||
expect(overrides).toEqual({
|
||||
prompt_tokens: 230_000 * Number(model.costs.prompt_tokens) * 2,
|
||||
cached_tokens: 50_000 * Number(model.costs.cached_tokens) * 2,
|
||||
cache_write_tokens:
|
||||
20_000 * Number(model.costs.cache_write_tokens) * 2,
|
||||
completion_tokens:
|
||||
10_000 * Number(model.costs.completion_tokens) * 1.5,
|
||||
});
|
||||
const totalCents =
|
||||
Object.values(overrides as Record<string, number>).reduce(
|
||||
(a, b) => a + b,
|
||||
0,
|
||||
) / 1_000_000;
|
||||
expect(totalCents).toBeCloseTo(total);
|
||||
},
|
||||
);
|
||||
|
||||
it('bills cached tokens at the input rate when the model prices no cache read', async () => {
|
||||
// gpt-5.4-pro is responses-API-only and its catalogue entry has no
|
||||
|
||||
@@ -31,6 +31,31 @@ const GPT_LONG_CONTEXT_PRICING = {
|
||||
|
||||
// Hardcoded from https://models.dev/api.json
|
||||
export const OPEN_AI_MODELS: IChatModel[] = [
|
||||
{
|
||||
puterId: 'openai:openai/gpt-6.1-terra',
|
||||
id: 'gpt-6.1-terra',
|
||||
responsesSampling: 'reasoningDisabled',
|
||||
modalities: { input: ['text', 'image'], output: ['text'] },
|
||||
open_weights: false,
|
||||
tool_call: true,
|
||||
knowledge: '2026-06-01',
|
||||
release_date: '2026-10-26',
|
||||
aliases: ['openai/gpt-6.1-terra'],
|
||||
costs_currency: 'usd-cents',
|
||||
input_cost_key: 'prompt_tokens',
|
||||
output_cost_key: 'completion_tokens',
|
||||
costs: {
|
||||
tokens: 1_000_000,
|
||||
prompt_tokens: 100,
|
||||
cached_tokens: 5,
|
||||
cache_write_tokens: 100 * 1.25,
|
||||
completion_tokens: 500,
|
||||
},
|
||||
long_context_pricing: GPT_LONG_CONTEXT_PRICING,
|
||||
context: 1_050_000,
|
||||
max_tokens: 128_000,
|
||||
responses_api: true,
|
||||
},
|
||||
{
|
||||
puterId: 'openai:openai/gpt-6.1-sol',
|
||||
id: 'gpt-6.1-sol',
|
||||
|
||||
@@ -289,6 +289,9 @@ describe('clampReasoningEffort', () => {
|
||||
['gpt-5.5', 'max', 'xhigh'],
|
||||
['gpt-5', 'high', 'high'],
|
||||
['gpt-6-astra-1', 'none', 'low'],
|
||||
...(['none', 'low', 'medium', 'high', 'xhigh', 'max'] as const).map(
|
||||
(effort) => ['gpt-6.1-terra', effort, effort] as const,
|
||||
),
|
||||
] as const)(
|
||||
'clamps %s requesting %s to %s',
|
||||
(modelId, requested, expected) => {
|
||||
|
||||
@@ -32,9 +32,9 @@ An object containing the following properties:
|
||||
- `signal` (AbortSignal) (Optional) - Abort the request, including while waiting for the next streamed chunk. Cancellation rejects with the signal reason (normally an `AbortError`).
|
||||
- `stream` (Boolean) - A boolean indicating whether you want to stream the completion. Defaults to `false`.
|
||||
- `max_tokens` (Number) - The maximum number of tokens to generate in the completion. By default, the specific model's maximum is used.
|
||||
- `temperature` (Number) - A number between 0 and 2 indicating the randomness of the completion. Lower values make the output more focused and deterministic, while higher values make it more random. By default, the model's own temperature is used. Some OpenAI reasoning models ignore `temperature` and `top_p`. GPT-5.6 and GPT-6 Sol/Luna accept them only with `reasoning_effort: 'none'`, and GPT-6 Astra, GPT-6.1 Sol, and GPT-5.3 Codex always ignore them. `gemini-4-argon` ignores `temperature` too. `gemini-3.1-flash-lite` and `gemini-3-flash-preview` use it.
|
||||
- `temperature` (Number) - A number between 0 and 2 indicating the randomness of the completion. Lower values make the output more focused and deterministic, while higher values make it more random. By default, the model's own temperature is used. Some OpenAI reasoning models ignore `temperature` and `top_p`. GPT-5.6, GPT-6 Sol/Luna, and GPT-6.1 Terra accept them only with `reasoning_effort: 'none'`, and GPT-6 Astra, GPT-6.1 Sol, and GPT-5.3 Codex always ignore them. `gemini-4-argon` ignores `temperature` too. `gemini-3.1-flash-lite` and `gemini-3-flash-preview` use it.
|
||||
- `tools` (Array) (Optional) - Function definitions the AI can call. See [Function Calling](#function-calling) for details.
|
||||
- `reasoning_effort` / `reasoning.effort` (String) (Optional) - Controls how much effort reasoning models spend thinking. Supported values: `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. Lower values give faster responses with less reasoning. OpenAI models, Meta's Muse Spark models, `gemini-4-argon`, `gemini-3.1-flash-lite` and `gemini-3-flash-preview` only; Muse Spark always reasons, so `none` is ignored for it. `gemini-4-argon` accepts `low`, `medium` and `high`, and uses `medium` when you leave it unset. `gemini-3.1-flash-lite` and `gemini-3-flash-preview` accept `minimal`, `low`, `medium` and `high`, and use Google's default level when you leave it unset. For these three Gemini models, any other value fails with `400 bad_request`, and messages can contain text and images only.
|
||||
- `reasoning_effort` / `reasoning.effort` (String) (Optional) - Controls how much effort reasoning models spend thinking. Supported values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. GPT-6.1 Terra accepts `none`, `low`, `medium` (default), `high`, `xhigh`, and `max`. Lower values give faster responses with less reasoning. OpenAI models, Meta's Muse Spark models, `gemini-4-argon`, `gemini-3.1-flash-lite` and `gemini-3-flash-preview` only; Muse Spark always reasons, so `none` is ignored for it. `gemini-4-argon` accepts `low`, `medium` and `high`, and uses `medium` when you leave it unset. `gemini-3.1-flash-lite` and `gemini-3-flash-preview` accept `minimal`, `low`, `medium` and `high`, and use Google's default level when you leave it unset. For these three Gemini models, any other value fails with `400 bad_request`, and messages can contain text and images only.
|
||||
- `verbosity` / `text.verbosity` (String) (Optional) - Controls how long or short responses are. Supported values: `low`, `medium`, and `high`. Lower values give shorter responses. OpenAI models only.
|
||||
- `normalize` (Boolean) (Optional) - Controls the format of the non-streaming response. When `true`, the response is normalized to the OpenAI format regardless of the model's vendor: `message.content` is a string, tool calls appear as `message.tool_calls`, and `finish_reason` is one of `stop`, `length`, `tool_calls`, or `content_filter` — or the vendor's own stop reason, passed through unchanged when it has no OpenAI equivalent. When `false`, the response keeps the vendor's native format (for Anthropic models, an array of content blocks). When unset, the SDK-wide `puter.ai.normalize` applies — itself tri-state: set it to `true` to normalize every call regardless of release date, `false` to disable normalization for every call, and when it is left unset too (the default) the release-date policy applies: **models released on or after September 1, 2026 return normalized (OpenAI-format) responses by default**, and for older models the default is unchanged — `message.content` keeps its vendor-native shape. (A handful of reasoning fields were made consistent across all models independently of this option; see [Reasoning fields on existing models](#reasoning-fields-on-existing-models).) Streaming responses are unaffected — chunks already share one format across vendors. See [Response normalization](#response-normalization).
|
||||
- `compaction` (Boolean | Object) (Optional) - Opt into inline context compaction for long conversations. Pass `true` to enable it with provider defaults, or `{ trigger_tokens: number }` to set the token threshold at which earlier context is summarized. When the model compacts, you receive a `compaction` chunk while streaming (or a `compaction` field on the result when not streaming) containing an opaque `encrypted_content` summary. Resend that item in `messages` on the next turn in place of the summarized history. The compaction chunk shape is identical across providers, so the same code works whether `model` is an OpenAI or Anthropic model. See [Compaction](#compaction).
|
||||
|
||||
@@ -77,6 +77,8 @@ An input over its limit is rejected before it reaches the model, as is an image
|
||||
|
||||
The standard chat timeout is 10 minutes for a response to start, or to finish if it isn't streamed. Stream durations depend on the provider. Native Gemini applies the 10-minute timeout to the whole response, including streaming. Groq uses 60 seconds to start a response. Mistral streams have no timeout of their own. If the caller disconnects, a stream that's still running gets 10 more minutes to finish, and is then stopped. See the [AI model directory](https://developer.puter.com/ai/models/) for model specifications.
|
||||
|
||||
GPT-6.1 Terra has a 1,050,000-token context window and a maximum output of 128,000 tokens. Requests with more than 272,000 input tokens are charged at twice the input and cache rates and 1.5 times the output rate for the whole request.
|
||||
|
||||
A `gemini-4-argon` response that isn't streamed is capped at 32,768 output tokens, and a larger `max_tokens` is lowered to that cap. Streaming uses the model-directory limit.
|
||||
|
||||
Text-to-speech calls to ElevenLabs, xAI, Speechify, Kling and Gemini time out after 2 minutes; xAI speech-to-text and voice-changer calls after 10 minutes, upload included. A call that times out before any audio or transcript comes back fails with `504 upstream_timeout`. For Gemini, the 2 minutes cover the whole reply, audio included.
|
||||
|
||||
Reference in new issue
Block a user