feat(ai): add input-based pricing tiers
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
### Added
|
||||
|
||||
- Added the opt-in `max` thinking level across CLI, SDK, RPC, model selection, and themes. Custom themes can define `thinkingMax`; existing themes fall back to `thinkingXhigh`.
|
||||
- Added request-wide input-token pricing tiers to custom model costs in `models.json`, `modelOverrides`, and extension-registered providers.
|
||||
|
||||
## [0.80.5] - 2026-07-09
|
||||
|
||||
|
||||
@@ -205,9 +205,31 @@ If your command is slow, expensive, rate-limited, or should keep using a previou
|
||||
| `input` | No | `["text"]` | Input types: `["text"]` or `["text", "image"]` |
|
||||
| `contextWindow` | No | `128000` | Context window size in tokens |
|
||||
| `maxTokens` | No | `16384` | Maximum output tokens |
|
||||
| `cost` | No | all zeros | `{"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}` (per million tokens) |
|
||||
| `cost` | No | all zeros | Per-million-token rates with optional request-wide input pricing tiers |
|
||||
| `compat` | No | provider `compat` | Provider compatibility overrides. Merged with provider-level `compat` when both are set. |
|
||||
|
||||
A cost tier supplies a complete alternate rate set and applies to the full request when total input usage (`input + cacheRead + cacheWrite`) exceeds `inputTokensAbove`. When multiple tiers match, the highest threshold wins.
|
||||
|
||||
```json
|
||||
{
|
||||
"cost": {
|
||||
"input": 5,
|
||||
"output": 30,
|
||||
"cacheRead": 0.5,
|
||||
"cacheWrite": 6.25,
|
||||
"tiers": [
|
||||
{
|
||||
"inputTokensAbove": 272000,
|
||||
"input": 10,
|
||||
"output": 45,
|
||||
"cacheRead": 1,
|
||||
"cacheWrite": 12.5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Current behavior:
|
||||
- `/model`, `--list-models`, and the interactive footer display entries by model `id`.
|
||||
- The configured `name` is used for model matching and secondary model detail text. It does not replace the footer/status-bar model id.
|
||||
@@ -317,6 +339,24 @@ Use `modelOverrides` to customize built-in models and matching extension-registe
|
||||
|
||||
`modelOverrides` supports these fields per model: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (partial), `contextWindow`, `maxTokens`, `headers`, `compat`.
|
||||
|
||||
Direct OpenAI GPT-5.6 Sol, Terra, and Luna default to a `272000` context window so requests remain within OpenAI's short-context pricing tier. To opt into OpenAI's 1.05M context window, increase it for each model you use:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openai": {
|
||||
"modelOverrides": {
|
||||
"gpt-5.6-sol": {
|
||||
"contextWindow": 1050000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The override preserves the built-in pricing metadata. Requests with more than 272K total input tokens use GPT-5.6's long-context rates for the entire request. Apply the same override to `gpt-5.6-terra` or `gpt-5.6-luna` when needed.
|
||||
|
||||
Behavior notes:
|
||||
- `modelOverrides` are applied to built-in provider models and matching extension-registered provider models.
|
||||
- Unknown model IDs are ignored.
|
||||
|
||||
@@ -1451,8 +1451,8 @@ export interface ProviderModelConfig {
|
||||
thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
|
||||
/** Supported input types. */
|
||||
input: ("text" | "image")[];
|
||||
/** Cost per token (for tracking, can be 0). */
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
/** Per-million-token cost rates and optional request-wide input pricing tiers. */
|
||||
cost: Model<Api>["cost"];
|
||||
/** Maximum context window size in tokens. */
|
||||
contextWindow: number;
|
||||
/** Maximum output tokens. */
|
||||
|
||||
@@ -156,6 +156,21 @@ const ProviderCompatSchema = Type.Union([
|
||||
AnthropicMessagesCompatSchema,
|
||||
]);
|
||||
|
||||
const ModelCostRatesSchema = {
|
||||
input: Type.Number(),
|
||||
output: Type.Number(),
|
||||
cacheRead: Type.Number(),
|
||||
cacheWrite: Type.Number(),
|
||||
};
|
||||
const ModelCostTierSchema = Type.Object({
|
||||
inputTokensAbove: Type.Number(),
|
||||
...ModelCostRatesSchema,
|
||||
});
|
||||
const ModelCostSchema = Type.Object({
|
||||
...ModelCostRatesSchema,
|
||||
tiers: Type.Optional(Type.Array(ModelCostTierSchema)),
|
||||
});
|
||||
|
||||
// Schema for custom model definition
|
||||
// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.)
|
||||
const ModelDefinitionSchema = Type.Object({
|
||||
@@ -166,14 +181,7 @@ const ModelDefinitionSchema = Type.Object({
|
||||
reasoning: Type.Optional(Type.Boolean()),
|
||||
thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema),
|
||||
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
||||
cost: Type.Optional(
|
||||
Type.Object({
|
||||
input: Type.Number(),
|
||||
output: Type.Number(),
|
||||
cacheRead: Type.Number(),
|
||||
cacheWrite: Type.Number(),
|
||||
}),
|
||||
),
|
||||
cost: Type.Optional(ModelCostSchema),
|
||||
contextWindow: Type.Optional(Type.Number()),
|
||||
maxTokens: Type.Optional(Type.Number()),
|
||||
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||
@@ -192,6 +200,7 @@ const ModelOverrideSchema = Type.Object({
|
||||
output: Type.Optional(Type.Number()),
|
||||
cacheRead: Type.Optional(Type.Number()),
|
||||
cacheWrite: Type.Optional(Type.Number()),
|
||||
tiers: Type.Optional(Type.Array(ModelCostTierSchema)),
|
||||
}),
|
||||
),
|
||||
contextWindow: Type.Optional(Type.Number()),
|
||||
@@ -335,6 +344,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
||||
output: override.cost.output ?? model.cost.output,
|
||||
cacheRead: override.cost.cacheRead ?? model.cost.cacheRead,
|
||||
cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite,
|
||||
tiers: override.cost.tiers ?? model.cost.tiers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -999,7 +1009,7 @@ export interface ProviderConfigInput {
|
||||
reasoning: boolean;
|
||||
thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
|
||||
input: ("text" | "image")[];
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
cost: Model<Api>["cost"];
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
headers?: Record<string, string>;
|
||||
|
||||
@@ -49,7 +49,21 @@ describe("ExtensionRunner", () => {
|
||||
name: "Instant Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 0.1,
|
||||
cacheWrite: 1.25,
|
||||
tiers: [
|
||||
{
|
||||
inputTokensAbove: 272000,
|
||||
input: 2,
|
||||
output: 3,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 2.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
@@ -859,7 +873,15 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
runtime.registerProvider("instant-provider", providerModelConfig);
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(0);
|
||||
expect(modelRegistry.find("instant-provider", "instant-model")).toBeDefined();
|
||||
expect(modelRegistry.find("instant-provider", "instant-model")?.cost.tiers).toEqual([
|
||||
{
|
||||
inputTokensAbove: 272000,
|
||||
input: 2,
|
||||
output: 3,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 2.5,
|
||||
},
|
||||
]);
|
||||
|
||||
runtime.unregisterProvider("instant-provider");
|
||||
expect(modelRegistry.find("instant-provider", "instant-model")).toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user