From 3d2bdfceefef8d07280d371262107b4c7ea97b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Kujund=C5=BEi=C4=87?= Date: Fri, 11 Sep 2026 14:47:41 +0200 Subject: [PATCH 1/3] fix: quote and pin Infron service tiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infron sells the same model at several service tiers. `min_prompt_price` and `min_completion_price` are the floor across all of them, so every model with a flex tier was advertised at a batch-job price nobody gets by default: 25 of 286 chat models, among them gpt-6-astra at $5/$25 against the $7.5/$37.5 a default request actually bills. Price each tier from its own row in `providers[]` and pin that tier on the request, so the price quoted is the price charged. Tiers beyond the default are listed under their own `:` ids, letting callers opt into flex or priority by model name: puter.ai.chat(prompt, { model: 'infron:openai/gpt-6-astra:flex' }) Suffix parsing matches the catalog exactly before reading a trailing segment as a tier, since catalog ids can carry a colon of their own (`deepseek/deepseek-v4-flash:free`). Tier variants take the context window of their own offering, which differs from the model-level figure for 11 of them. Billing was never wrong — it bills Infron's reported `cost` — but the understated prices fed the credit gate's output cap, which let a request run roughly 50% past the balance it was gated against, and the fallback path that prices per token when a response carries no cost. Co-Authored-By: Claude Opus 5 (1M context) --- .../providers/infron/InfronProvider.test.ts | 217 +++++++++++++++++- .../providers/infron/InfronProvider.ts | 204 ++++++++++++---- 2 files changed, 378 insertions(+), 43 deletions(-) diff --git a/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts b/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts index 3358fb3cb..0765a82b3 100644 --- a/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts @@ -120,6 +120,82 @@ const SAMPLE_API_MODELS = [ min_prompt_price: 2, min_completion_price: 10, }, + { + // Sold at several service tiers; the flex tier undercuts standard. + id: 'openai/gpt-6-astra', + display_name: 'OpenAI: GPT-6 Astra', + category_type: 'LLM', + supported_endpoint_types: ['openai'], + context_length: 1050000, + max_output_tokens: 128000, + min_prompt_price: 5, + min_completion_price: 25, + providers: [ + { + provider_slug: 'openai/flex', + service_tier: 'flex', + prompt_price: 5, + completion_price: 25, + context_length: 400000, + }, + { + provider_slug: 'azure', + service_tier: 'standard', + prompt_price: 7.5, + completion_price: 37.5, + }, + { + provider_slug: 'openai', + service_tier: 'standard', + prompt_price: 10, + completion_price: 50, + }, + { + provider_slug: 'openai/priority', + service_tier: 'priority', + prompt_price: 20, + completion_price: 100, + }, + ], + }, + { + // Flex is the only tier on offer — nothing to pin. + id: 'example/flex-only-model', + display_name: 'Flex Only', + category_type: 'LLM', + supported_endpoint_types: ['openai'], + context_length: 128000, + max_output_tokens: 4096, + min_prompt_price: 1, + min_completion_price: 2, + providers: [ + { + provider_slug: 'example/flex', + service_tier: 'flex', + prompt_price: 1, + completion_price: 2, + }, + ], + }, + { + // Catalog ids can contain a colon of their own. + id: 'example/colon-model:free', + display_name: 'Colon Model (free)', + category_type: 'LLM', + supported_endpoint_types: ['openai'], + context_length: 64000, + max_output_tokens: 4096, + min_prompt_price: 0, + min_completion_price: 0, + providers: [ + { + provider_slug: 'example', + service_tier: 'standard', + prompt_price: 0, + completion_price: 0, + }, + ], + }, { // Non-chat modality — filtered out. id: 'black-forest-labs/flux-2.1', @@ -262,10 +338,87 @@ describe('InfronProvider model catalog', () => { expect(axiosRequestMock).toHaveBeenCalledTimes(1); }); + it('quotes the tier Infron routes to by default, not the cheaper flex tier', async () => { + const { provider } = makeProvider(); + const models = await provider.models(); + // Cheapest standard offering is Azure at $7.5/$37.5 per million, + // not the $5/$25 flex tier the catalog floor reports. + expect(models).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'infron:openai/gpt-6-astra', + costs: expect.objectContaining({ + prompt: 750, + completion: 3750, + input_cache_read: 750, + }), + }), + ]), + ); + }); + + it('lists a :flex id alongside the default-tier id, each at its own price', async () => { + const { provider } = makeProvider(); + const models = await provider.models(); + + expect(models).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'infron:openai/gpt-6-astra', + costs: expect.objectContaining({ + prompt: 750, + completion: 3750, + }), + }), + expect.objectContaining({ + id: 'infron:openai/gpt-6-astra:flex', + costs: expect.objectContaining({ + prompt: 500, + completion: 2500, + }), + }), + expect.objectContaining({ + id: 'infron:openai/gpt-6-astra:priority', + costs: expect.objectContaining({ + prompt: 2000, + completion: 10000, + }), + }), + ]), + ); + }); + + it('takes a tier variant context window from its own offering', async () => { + const { provider } = makeProvider(); + const models = await provider.models(); + const flex = models.find( + (m) => m.id === 'infron:openai/gpt-6-astra:flex', + ); + // The flex offering caps context below the model-level figure. + expect(flex?.context).toBe(400000); + }); + + it('accepts the explicit :standard spelling as an alias of the plain id', async () => { + const { provider } = makeProvider(); + const models = await provider.models(); + const base = models.find((m) => m.id === 'infron:openai/gpt-6-astra'); + expect(base?.aliases).toContain('openai/gpt-6-astra:standard'); + }); + + it('still offers an explicit tier id when no default tier is sold', async () => { + const { provider } = makeProvider(); + const ids = await provider.list(); + // The plain id leaves routing to Infron; the :flex id pins the one + // tier on offer. Same price, different routing guarantee. + expect(ids).toContain('infron:example/flex-only-model'); + expect(ids).toContain('infron:example/flex-only-model:flex'); + }); + it('converts USD-per-million-token prices to microcents per token', async () => { const { provider } = makeProvider(); const models = await provider.models(); - // $10/M tokens → 10 * 100 = 1000 microcents per token. + // $10/M tokens → 10 * 100 = 1000 microcents per token. This model + // carries no per-tier breakdown, so the catalog floor is used. expect(models).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -314,6 +467,68 @@ describe('InfronProvider.complete request shape', () => { expect(args.usage).toEqual({ include: true }); }); + it('pins the standard service tier so billing matches the quoted price', async () => { + const { provider } = makeProvider(); + createMock.mockResolvedValueOnce(baseCompletion); + + await withTestActor(() => + provider.complete({ + model: 'infron:openai/gpt-6-astra', + messages: [{ role: 'user', content: 'hello' }], + }), + ); + + expect(createMock.mock.calls[0]![0].provider).toEqual({ + service_tier: 'standard', + }); + }); + + it('leaves routing to Infron for models with no standard tier', async () => { + const { provider } = makeProvider(); + createMock.mockResolvedValueOnce(baseCompletion); + + await withTestActor(() => + provider.complete({ + model: 'infron:example/flex-only-model', + messages: [{ role: 'user', content: 'hello' }], + }), + ); + + expect('provider' in createMock.mock.calls[0]![0]).toBe(false); + }); + + it('pins the tier named by a :flex model id and strips it from the wire id', async () => { + const { provider } = makeProvider(); + createMock.mockResolvedValueOnce(baseCompletion); + + await withTestActor(() => + provider.complete({ + model: 'infron:openai/gpt-6-astra:flex', + messages: [{ role: 'user', content: 'hello' }], + }), + ); + + const [args] = createMock.mock.calls[0]!; + expect(args.model).toBe('openai/gpt-6-astra'); + expect(args.provider).toEqual({ service_tier: 'flex' }); + }); + + it('keeps a colon-bearing catalog id intact rather than reading it as a tier', async () => { + const { provider } = makeProvider(); + createMock.mockResolvedValueOnce(baseCompletion); + + await withTestActor(() => + provider.complete({ + model: 'infron:example/colon-model:free', + messages: [{ role: 'user', content: 'hello' }], + }), + ); + + const [args] = createMock.mock.calls[0]!; + expect(args.model).toBe('example/colon-model:free'); + expect(args.provider).toEqual({ service_tier: 'standard' }); + }); + it('only sets stream_options.include_usage when streaming', async () => { const { provider } = makeProvider(); diff --git a/src/backend/drivers/ai-chat/providers/infron/InfronProvider.ts b/src/backend/drivers/ai-chat/providers/infron/InfronProvider.ts index b46fa97ec..90ec23eb6 100644 --- a/src/backend/drivers/ai-chat/providers/infron/InfronProvider.ts +++ b/src/backend/drivers/ai-chat/providers/infron/InfronProvider.ts @@ -31,12 +31,25 @@ import type { ICompleteArguments, } from '../../types.js'; +/** + * One upstream offering of a model in Infron's catalog. The same model is often + * served at several service tiers with different prices; `flex` is + * batch-oriented and cheapest, `priority` is the fastest and dearest. + */ +type InfronApiProviderOffer = { + provider_slug?: string; + service_tier?: string; + prompt_price?: number; + completion_price?: number; + context_length?: number; +}; + /** * Shape of one entry in Infron's `GET /v1/models` catalog. Unlike OpenRouter - * there is no `pricing` object; prices are USD per million tokens in - * `min_prompt_price` / `min_completion_price`, and the catalog mixes non-chat - * modalities (image, video, embeddings) that this provider filters out via - * `category_type`. + * there is no `pricing` object; prices are USD per million tokens, per offering + * in `providers` and as a catalog-wide floor in `min_prompt_price` / + * `min_completion_price`. The catalog mixes non-chat modalities (image, video, + * embeddings) that this provider filters out via `category_type`. */ type InfronApiModel = { id: string; @@ -49,6 +62,117 @@ type InfronApiModel = { min_prompt_price?: number; min_completion_price?: number; min_request_price?: number; + providers?: InfronApiProviderOffer[]; +}; + +/** Tier Infron routes to when a request carries no explicit `service_tier`. */ +const DEFAULT_SERVICE_TIER = 'standard'; + +/** What one tier of a model costs, in USD per million tokens. */ +type InfronTierPrices = { + prompt: number; + completion: number; + context?: number; +}; + +/** + * Cheapest offering of each service tier a model sells. Tiers differ in price + * and sometimes in context window, so each one is quoted from its own row + * rather than from the catalog-wide `min_*` floor. + */ +const pricesByTier = (model: InfronApiModel) => { + const byTier = new Map(); + for (const offer of model.providers ?? []) { + const tier = offer.service_tier; + if (!tier) continue; + const prompt = offer.prompt_price ?? 0; + const completion = offer.completion_price ?? 0; + const seen = byTier.get(tier); + if (seen && seen.prompt + seen.completion <= prompt + completion) { + continue; + } + byTier.set(tier, { prompt, completion, context: offer.context_length }); + } + return byTier; +}; + +/** + * Splits a model id into the id Infron expects on the wire and the tier to pin. + * Catalog ids can contain a colon themselves (`…:free`), so an exact catalog + * match always wins over reading the last segment as a tier suffix. + */ +const resolveTier = (id: string, catalog: InfronApiModel[]) => { + const tiersOf = (model: InfronApiModel) => + new Set((model.providers ?? []).map((offer) => offer.service_tier)); + + const exact = catalog.find((model) => model.id === id); + if (exact) { + return { + wireModelId: id, + // Unsuffixed ids are quoted at the default tier, so pin it — left + // unset, Infron load-balances across tiers and could bill another. + tier: tiersOf(exact).has(DEFAULT_SERVICE_TIER) + ? DEFAULT_SERVICE_TIER + : undefined, + }; + } + + const cut = id.lastIndexOf(':'); + const base = + cut > 0 ? catalog.find((m) => m.id === id.slice(0, cut)) : undefined; + const tier = id.slice(cut + 1); + if (base && tiersOf(base).has(tier)) return { wireModelId: base.id, tier }; + return { wireModelId: id, tier: undefined }; +}; + +/** + * One listed model: the default tier under the plain id, or a single service + * tier under a `…:` id priced from that tier's own offering. + */ +const coerceModel = ( + model: InfronApiModel, + prices: InfronTierPrices, + tier?: string, +): IChatModel => { + const suffix = tier ? `:${tier}` : ''; + const shortId = model.id.split('/').slice(1).join('/'); + // Catalog prices are USD per million tokens; costs are microcents per + // token, so the conversion is ×100. + const promptCost = Math.round(prices.prompt * 100); + return { + id: `infron:${model.id}${suffix}`, + name: `${model.display_name || model.id} (Infron${tier ? `, ${tier}` : ''})`, + aliases: [ + `${model.id}${suffix}`, + ...(model.display_name && !tier ? [model.display_name] : []), + `infron/${model.id}${suffix}`, + `${shortId}${suffix}`, + // The plain id already means the default tier; accept the + // explicit spelling of it too. + ...(tier ? [] : [`${model.id}:${DEFAULT_SERVICE_TIER}`]), + ], + context: tier + ? (prices.context ?? model.context_length) + : model.context_length, + max_tokens: model.max_output_tokens ?? 0, + costs_currency: 'usd-cents', + input_cost_key: 'prompt', + output_cost_key: 'completion', + costs: { + tokens: 1_000_000, + prompt: promptCost, + completion: Math.round(prices.completion * 100), + // The catalog carries no cache-read price; charge the full + // prompt rate in the fallback path so cached tokens are never + // billed below list. The normal path bills the + // gateway-reported cost instead. + input_cache_read: promptCost, + // USD per request → microcents per request. + request: Math.round( + (model.min_request_price ?? 0) * 1_000_000 * 100, + ), + }, + }; }; type InfronUsage = OpenAI.Completions.CompletionUsage & { @@ -112,17 +236,25 @@ export class InfronProvider implements IChatProvider { [m.id, ...(m.aliases || [])].includes(model), ) || availableModels.find((m) => m.id === this.getDefaultModel())!; - const modelIdForParams = modelUsed.id.startsWith('infron:') + const catalogId = modelUsed.id.startsWith('infron:') ? modelUsed.id.slice('infron:'.length) : modelUsed.id; + // A `…:` id carries the tier to pin; a plain id means the + // default tier. Only tiers the model actually sells are pinned — + // Infron routes freely (and reports the tier back) when unset. + const { wireModelId, tier } = resolveTier( + catalogId, + await this.#rawModels(), + ); + const actor = Context.get('actor'); messages = await OpenAIUtil.process_input_messages(messages); const completionParams = { messages, - model: modelIdForParams, + model: wireModelId, ...(tools ? { tools } : {}), max_tokens, temperature, @@ -132,6 +264,10 @@ export class InfronProvider implements IChatProvider { stream_options: { include_usage: true }, } : {}), + // Without this Infron load-balances across service tiers, so a + // request could be billed at a tier other than the one whose + // price we quote in the catalog. + ...(tier ? { provider: { service_tier: tier } } : {}), // Surfaces the authoritative `cost` field (USD) on the // response so metering doesn't depend on catalog prices. usage: { include: true }, @@ -216,7 +352,8 @@ export class InfronProvider implements IChatProvider { }); } - async models() { + /** The catalog as Infron returns it, kv-cached and shared by callers. */ + async #rawModels(): Promise { let models = kv.get(KV_MODELS_KEY) as InfronApiModel[] | undefined; if (!models) { try { @@ -235,7 +372,11 @@ export class InfronProvider implements IChatProvider { console.log(e); } } - if (!models) return []; + return models ?? []; + } + + async models() { + const models = await this.#rawModels(); const coerced_models: IChatModel[] = []; for (const model of models) { // The catalog mixes chat with image/video/embedding/search @@ -245,40 +386,19 @@ export class InfronProvider implements IChatProvider { if (!(model.supported_endpoint_types ?? []).includes('openai')) { continue; } - // Catalog prices are USD per million tokens; costs are - // microcents per token, so the conversion is ×100. - const promptCost = Math.round((model.min_prompt_price ?? 0) * 100); - coerced_models.push({ - id: `infron:${model.id}`, - name: `${model.display_name || model.id} (Infron)`, - aliases: [ - model.id, - ...(model.display_name ? [model.display_name] : []), - `infron/${model.id}`, - model.id.split('/').slice(1).join('/'), - ], - context: model.context_length, - max_tokens: model.max_output_tokens ?? 0, - costs_currency: 'usd-cents', - input_cost_key: 'prompt', - output_cost_key: 'completion', - costs: { - tokens: 1_000_000, - prompt: promptCost, - completion: Math.round( - (model.min_completion_price ?? 0) * 100, - ), - // The catalog carries no cache-read price; charge the - // full prompt rate in the fallback path so cached - // tokens are never billed below list. The normal path - // bills the gateway-reported cost instead. - input_cache_read: promptCost, - // USD per request → microcents per request. - request: Math.round( - (model.min_request_price ?? 0) * 1_000_000 * 100, - ), - }, - }); + const byTier = pricesByTier(model); + const defaultPrices = byTier.get(DEFAULT_SERVICE_TIER) ?? { + prompt: model.min_prompt_price ?? 0, + completion: model.min_completion_price ?? 0, + }; + // The unsuffixed id is the default tier; every other tier the + // model sells gets its own `…:` id so callers can ask for + // one by name and see what it costs. + coerced_models.push(coerceModel(model, defaultPrices)); + for (const [tier, prices] of byTier) { + if (tier === DEFAULT_SERVICE_TIER) continue; + coerced_models.push(coerceModel(model, prices, tier)); + } } return coerced_models; } From 5fbdc7cac5766388e983e0780d441dbb7767f0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Kujund=C5=BEi=C4=87?= Date: Fri, 11 Sep 2026 14:52:28 +0200 Subject: [PATCH 2/3] docs: document Infron service tiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tiers beyond a model's default are reachable as `:` ids, so the chat docs gain a Service tiers section covering what the suffixes mean, that the tier named is the tier billed, and that a tier can carry a smaller context window than its siblings. The listModels return value described a `cost` object with `input` and `output` keys. No such field is returned — pricing comes back as `costs` alongside `costs_currency`, keyed per vendor and named by the model's own `input_cost_key` / `output_cost_key`. Correct the prose and replace the example with a real entry. Co-Authored-By: Claude Opus 5 (1M context) --- src/docs/src/AI/chat.md | 30 +++++++++++++++++++++++++++++- src/docs/src/AI/listModels.md | 23 +++++++++++++++-------- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/docs/src/AI/chat.md b/src/docs/src/AI/chat.md index 487d87b66..0cc38d389 100755 --- a/src/docs/src/AI/chat.md +++ b/src/docs/src/AI/chat.md @@ -27,7 +27,7 @@ A string containing the prompt you want to complete. An object containing the following properties: -- `model` (String) - The model you want to use for the completion. If not specified, defaults to `gpt-5-nano`. More than 500 models are available from vendors including OpenAI, Anthropic, Google, Alibaba Cloud, xAI, Mistral, OpenRouter, Infron, and others. For a full list, see the [AI models list](https://developer.puter.com/ai/models/) page. +- `model` (String) - The model you want to use for the completion. If not specified, defaults to `gpt-5-nano`. More than 500 models are available from vendors including OpenAI, Anthropic, Google, Alibaba Cloud, xAI, Mistral, OpenRouter, Infron, and others. For a full list, see the [AI models list](https://developer.puter.com/ai/models/) page. Models sold at more than one service tier accept a tier suffix on the id — see [Service tiers](#service-tiers). - `provider` (String) (Optional) - Pin the request to a specific vendor, for example `openrouter` or `infron`. Without it, Puter selects a vendor for the requested model. Call [`puter.ai.listModelProviders()`](/AI/listModelProviders) for the available values, and [`puter.ai.listModels(provider)`](/AI/listModels) for the models a given vendor serves. - `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. @@ -115,6 +115,34 @@ In case of an error, the `Promise` will reject with an error message. We use different vendors for different models and try to use the best vendor available at the time of the request. Vendors currently include Alibaba Cloud, Anthropic, Azure OpenAI, DeepSeek, Google, Infron, Meta, MiniMax, Mistral, Moonshot AI, OpenAI, OpenRouter, Together AI, xAI, and Z.AI. Call [`puter.ai.listModelProviders()`](/AI/listModelProviders) for the current list, or pass `provider` in the options object to pin a request to one of them. +## Service tiers + +Some vendors sell the same model at more than one service tier, trading latency against price. Where they do, Puter exposes each tier as its own model id: append `:flex` or `:priority` to the model name. + +```js +// Default tier +const a = await puter.ai.chat("Hello", { model: "infron:openai/gpt-6-astra" }); + +// Same model, flex tier — cheaper, with higher and less predictable latency +const b = await puter.ai.chat("Hello", { model: "infron:openai/gpt-6-astra:flex" }); +``` + +A plain model id always means the model's default tier, which is the balanced one. `flex` is the cheapest and is meant for work that tolerates waiting — batch jobs, overnight runs — because requests may be held until capacity frees up. `priority` is the fastest and the most expensive. + +The tier named in the id is the tier the request is billed at, and the price [`puter.ai.listModels()`](/AI/listModels) reports for that id is the price you pay for it. + +Not every model sells every tier, and a tier can differ from its siblings in more than price — some carry a smaller context window. Each tier is listed as a separate entry, so compare them before picking one: + +```js +const models = await puter.ai.listModels("infron"); +for (const m of models.filter(m => m.id.startsWith("infron:openai/gpt-6-astra"))) { + // Cost keys differ by vendor, so read them through input_cost_key / output_cost_key. + console.log(m.id, m.costs[m.input_cost_key], m.costs[m.output_cost_key], m.context); +} +``` + +If a model has no tier-suffixed ids in that list, it is sold at a single tier and the plain id is the only way to call it. + ## Response Normalization Most vendors respond in the OpenAI chat format, where `message.content` is a string and tool calls appear as `message.tool_calls`. Anthropic models historically respond in Anthropic's native format instead, where `message.content` is an array of content blocks such as `[{ type: "text", text: "..." }]`. diff --git a/src/docs/src/AI/listModels.md b/src/docs/src/AI/listModels.md index bd5193c20..6ef93dab6 100644 --- a/src/docs/src/AI/listModels.md +++ b/src/docs/src/AI/listModels.md @@ -20,24 +20,31 @@ A string containing the provider you want to list the models for. ## Return value -Resolves to an array of model objects. Each object always contains `id` and `provider`, and may include fields such as `name`, `aliases`, `context`, `max_tokens`, and a `cost` object (`currency`, `tokens`, `input` and `output` costs in cents). Additional provider-specific capability fields may also be present. +Resolves to an array of model objects. Each object always contains `id` and `provider`, and may include fields such as `name`, `aliases`, `context` and `max_tokens`. + +Pricing is reported as a `costs` object alongside `costs_currency`. The keys of `costs` differ by vendor, so each model names its own with `input_cost_key` and `output_cost_key`; read prices through those rather than hard-coding a key. `costs.tokens` is the number of tokens the prices are quoted per — 1,000,000 throughout — and the remaining keys cover vendor-specific extras such as cache reads. Additional provider-specific capability fields may also be present. + +A model sold at several service tiers appears once per tier, under ids suffixed with the tier name (`…:flex`, `…:priority`), each carrying its own `costs` and `context`. See [Service tiers](/AI/chat#service-tiers) for how to pick one. Example model entry: ```json [ { - "id": "claude-opus-4-8", + "id": "claude-opus-4-5-20251101", "provider": "claude", - "name": "Claude Opus 4.8", - "aliases": ["claude-opus-4-8-latest"], + "name": "Claude Opus 4.5", + "aliases": ["claude-opus-4-5-latest", "claude-opus-4-5"], "context": 200000, "max_tokens": 64000, - "cost": { - "currency": "usd-cents", + "costs_currency": "usd-cents", + "input_cost_key": "input_tokens", + "output_cost_key": "output_tokens", + "costs": { "tokens": 1000000, - "input": 500, - "output": 2500 + "input_tokens": 500, + "cache_read_input_tokens": 50, + "output_tokens": 2500 } } ] From ab077f047bf73226ec7b2721a4cd31f13a86769a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Kujund=C5=BEi=C4=87?= Date: Fri, 11 Sep 2026 21:32:18 +0200 Subject: [PATCH 3/3] Revert "docs: document Infron service tiers" This reverts commit 5fbdc7cac5766388e983e0780d441dbb7767f0cd. Docs will land in a separate PR. Co-Authored-By: Claude Opus 5 (1M context) --- src/docs/src/AI/chat.md | 30 +----------------------------- src/docs/src/AI/listModels.md | 23 ++++++++--------------- 2 files changed, 9 insertions(+), 44 deletions(-) diff --git a/src/docs/src/AI/chat.md b/src/docs/src/AI/chat.md index 0cc38d389..487d87b66 100755 --- a/src/docs/src/AI/chat.md +++ b/src/docs/src/AI/chat.md @@ -27,7 +27,7 @@ A string containing the prompt you want to complete. An object containing the following properties: -- `model` (String) - The model you want to use for the completion. If not specified, defaults to `gpt-5-nano`. More than 500 models are available from vendors including OpenAI, Anthropic, Google, Alibaba Cloud, xAI, Mistral, OpenRouter, Infron, and others. For a full list, see the [AI models list](https://developer.puter.com/ai/models/) page. Models sold at more than one service tier accept a tier suffix on the id — see [Service tiers](#service-tiers). +- `model` (String) - The model you want to use for the completion. If not specified, defaults to `gpt-5-nano`. More than 500 models are available from vendors including OpenAI, Anthropic, Google, Alibaba Cloud, xAI, Mistral, OpenRouter, Infron, and others. For a full list, see the [AI models list](https://developer.puter.com/ai/models/) page. - `provider` (String) (Optional) - Pin the request to a specific vendor, for example `openrouter` or `infron`. Without it, Puter selects a vendor for the requested model. Call [`puter.ai.listModelProviders()`](/AI/listModelProviders) for the available values, and [`puter.ai.listModels(provider)`](/AI/listModels) for the models a given vendor serves. - `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. @@ -115,34 +115,6 @@ In case of an error, the `Promise` will reject with an error message. We use different vendors for different models and try to use the best vendor available at the time of the request. Vendors currently include Alibaba Cloud, Anthropic, Azure OpenAI, DeepSeek, Google, Infron, Meta, MiniMax, Mistral, Moonshot AI, OpenAI, OpenRouter, Together AI, xAI, and Z.AI. Call [`puter.ai.listModelProviders()`](/AI/listModelProviders) for the current list, or pass `provider` in the options object to pin a request to one of them. -## Service tiers - -Some vendors sell the same model at more than one service tier, trading latency against price. Where they do, Puter exposes each tier as its own model id: append `:flex` or `:priority` to the model name. - -```js -// Default tier -const a = await puter.ai.chat("Hello", { model: "infron:openai/gpt-6-astra" }); - -// Same model, flex tier — cheaper, with higher and less predictable latency -const b = await puter.ai.chat("Hello", { model: "infron:openai/gpt-6-astra:flex" }); -``` - -A plain model id always means the model's default tier, which is the balanced one. `flex` is the cheapest and is meant for work that tolerates waiting — batch jobs, overnight runs — because requests may be held until capacity frees up. `priority` is the fastest and the most expensive. - -The tier named in the id is the tier the request is billed at, and the price [`puter.ai.listModels()`](/AI/listModels) reports for that id is the price you pay for it. - -Not every model sells every tier, and a tier can differ from its siblings in more than price — some carry a smaller context window. Each tier is listed as a separate entry, so compare them before picking one: - -```js -const models = await puter.ai.listModels("infron"); -for (const m of models.filter(m => m.id.startsWith("infron:openai/gpt-6-astra"))) { - // Cost keys differ by vendor, so read them through input_cost_key / output_cost_key. - console.log(m.id, m.costs[m.input_cost_key], m.costs[m.output_cost_key], m.context); -} -``` - -If a model has no tier-suffixed ids in that list, it is sold at a single tier and the plain id is the only way to call it. - ## Response Normalization Most vendors respond in the OpenAI chat format, where `message.content` is a string and tool calls appear as `message.tool_calls`. Anthropic models historically respond in Anthropic's native format instead, where `message.content` is an array of content blocks such as `[{ type: "text", text: "..." }]`. diff --git a/src/docs/src/AI/listModels.md b/src/docs/src/AI/listModels.md index 6ef93dab6..bd5193c20 100644 --- a/src/docs/src/AI/listModels.md +++ b/src/docs/src/AI/listModels.md @@ -20,31 +20,24 @@ A string containing the provider you want to list the models for. ## Return value -Resolves to an array of model objects. Each object always contains `id` and `provider`, and may include fields such as `name`, `aliases`, `context` and `max_tokens`. - -Pricing is reported as a `costs` object alongside `costs_currency`. The keys of `costs` differ by vendor, so each model names its own with `input_cost_key` and `output_cost_key`; read prices through those rather than hard-coding a key. `costs.tokens` is the number of tokens the prices are quoted per — 1,000,000 throughout — and the remaining keys cover vendor-specific extras such as cache reads. Additional provider-specific capability fields may also be present. - -A model sold at several service tiers appears once per tier, under ids suffixed with the tier name (`…:flex`, `…:priority`), each carrying its own `costs` and `context`. See [Service tiers](/AI/chat#service-tiers) for how to pick one. +Resolves to an array of model objects. Each object always contains `id` and `provider`, and may include fields such as `name`, `aliases`, `context`, `max_tokens`, and a `cost` object (`currency`, `tokens`, `input` and `output` costs in cents). Additional provider-specific capability fields may also be present. Example model entry: ```json [ { - "id": "claude-opus-4-5-20251101", + "id": "claude-opus-4-8", "provider": "claude", - "name": "Claude Opus 4.5", - "aliases": ["claude-opus-4-5-latest", "claude-opus-4-5"], + "name": "Claude Opus 4.8", + "aliases": ["claude-opus-4-8-latest"], "context": 200000, "max_tokens": 64000, - "costs_currency": "usd-cents", - "input_cost_key": "input_tokens", - "output_cost_key": "output_tokens", - "costs": { + "cost": { + "currency": "usd-cents", "tokens": 1000000, - "input_tokens": 500, - "cache_read_input_tokens": 50, - "output_tokens": 2500 + "input": 500, + "output": 2500 } } ]