mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-07-09 17:28:45 +00:00
fix(coding-agent): apply modelOverrides to extension providers
closes #6367
This commit is contained in:
parent
4285712bae
commit
c6251a866b
4 changed files with 82 additions and 7 deletions
|
|
@ -20,6 +20,7 @@
|
|||
- Fixed the edit tool schema to allow model-invented extra replacement fields instead of rejecting otherwise valid edits ([#6278](https://github.com/earendil-works/pi/issues/6278)).
|
||||
- Fixed new session resets to clear cached label timestamps ([#6354](https://github.com/earendil-works/pi/issues/6354)).
|
||||
- Fixed auto-retry for Bun fetch socket-drop errors reported as `socket connection was closed`, so transient provider disconnects do not end headless runs without retrying ([#6431](https://github.com/earendil-works/pi/issues/6431)).
|
||||
- Fixed `models.json` `modelOverrides` to apply to extension-registered provider models ([#6367](https://github.com/earendil-works/pi/issues/6367)).
|
||||
|
||||
### Removed
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ Set `api` at provider level (default for all models) or model level (override pe
|
|||
| `headers` | Custom headers (see value resolution below) |
|
||||
| `authHeader` | Set `true` to add `Authorization: Bearer <apiKey>` automatically |
|
||||
| `models` | Array of model configurations |
|
||||
| `modelOverrides` | Per-model overrides for built-in models on this provider |
|
||||
| `modelOverrides` | Per-model overrides for built-in or extension-registered models on this provider |
|
||||
|
||||
For providers with `models`, non-built-in provider configs need `baseUrl` and an `api` value at either provider or model level. `apiKey` is not required to load the file: models become available when auth is configured through `/login`/`auth.json`, CLI `--api-key`, or provider `apiKey`. If no auth is configured, the models load but stay unavailable in `/model` and `--list-models`.
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ Merge semantics:
|
|||
|
||||
## Per-model Overrides
|
||||
|
||||
Use `modelOverrides` to customize specific built-in models without replacing the provider's full model list.
|
||||
Use `modelOverrides` to customize built-in models and matching extension-registered models without replacing the provider's full model list.
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -314,10 +314,10 @@ Use `modelOverrides` to customize specific built-in models without replacing the
|
|||
}
|
||||
```
|
||||
|
||||
`modelOverrides` supports these fields per model: `name`, `reasoning`, `input`, `cost` (partial), `contextWindow`, `maxTokens`, `headers`, `compat`.
|
||||
`modelOverrides` supports these fields per model: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (partial), `contextWindow`, `maxTokens`, `headers`, `compat`.
|
||||
|
||||
Behavior notes:
|
||||
- `modelOverrides` are applied to built-in provider models.
|
||||
- `modelOverrides` are applied to built-in provider models and matching extension-registered provider models.
|
||||
- Unknown model IDs are ignored.
|
||||
- You can combine provider-level `baseUrl`/`headers` with `modelOverrides`.
|
||||
- Overriding `name` changes model matching and secondary detail text only; the footer and primary model lists continue to show the model `id`.
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ export class ModelRegistry {
|
|||
private models: Model<Api>[] = [];
|
||||
private providerRequestConfigs: Map<string, ProviderRequestConfig> = new Map();
|
||||
private modelRequestHeaders: Map<string, Record<string, string>> = new Map();
|
||||
private configModelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
|
||||
private registeredProviders: Map<string, ProviderConfigInput> = new Map();
|
||||
private loadError: string | undefined = undefined;
|
||||
readonly authStorage: AuthStorage;
|
||||
|
|
@ -406,6 +407,7 @@ export class ModelRegistry {
|
|||
modelOverrides,
|
||||
error,
|
||||
} = this.modelsJsonPath ? this.loadCustomModels(this.modelsJsonPath) : emptyCustomModelsResult();
|
||||
this.configModelOverrides = modelOverrides;
|
||||
|
||||
if (error) {
|
||||
this.loadError = error;
|
||||
|
|
@ -459,6 +461,15 @@ export class ModelRegistry {
|
|||
});
|
||||
}
|
||||
|
||||
private getConfiguredModelOverride(providerName: string, modelId: string): ModelOverride | undefined {
|
||||
return this.configModelOverrides.get(providerName)?.get(modelId);
|
||||
}
|
||||
|
||||
private applyConfiguredModelOverride(providerName: string, model: Model<Api>): Model<Api> {
|
||||
const modelOverride = this.getConfiguredModelOverride(providerName, model.id);
|
||||
return modelOverride ? applyModelOverride(model, modelOverride) : model;
|
||||
}
|
||||
|
||||
/** Merge custom models into built-in list by provider+id (custom wins on conflicts). */
|
||||
private mergeCustomModels(builtInModels: Model<Api>[], customModels: Model<Api>[]): Model<Api>[] {
|
||||
const merged = [...builtInModels];
|
||||
|
|
@ -921,9 +932,14 @@ export class ModelRegistry {
|
|||
// Parse and add new models
|
||||
for (const modelDef of config.models) {
|
||||
const api = modelDef.api || config.api;
|
||||
this.storeModelHeaders(providerName, modelDef.id, modelDef.headers);
|
||||
const modelOverride = this.getConfiguredModelOverride(providerName, modelDef.id);
|
||||
const headers =
|
||||
modelDef.headers || modelOverride?.headers
|
||||
? { ...modelDef.headers, ...modelOverride?.headers }
|
||||
: undefined;
|
||||
this.storeModelHeaders(providerName, modelDef.id, headers);
|
||||
|
||||
this.models.push({
|
||||
const model = this.applyConfiguredModelOverride(providerName, {
|
||||
id: modelDef.id,
|
||||
name: modelDef.name,
|
||||
api: api as Api,
|
||||
|
|
@ -938,6 +954,7 @@ export class ModelRegistry {
|
|||
headers: undefined,
|
||||
compat: modelDef.compat,
|
||||
} as Model<Api>);
|
||||
this.models.push(model);
|
||||
}
|
||||
|
||||
// Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
Model,
|
||||
OpenAICompletionsCompat,
|
||||
} from "@earendil-works/pi-ai/compat";
|
||||
import { getApiProvider } from "@earendil-works/pi-ai/compat";
|
||||
import { getApiProvider, getSupportedThinkingLevels } from "@earendil-works/pi-ai/compat";
|
||||
import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { AuthStorage } from "../src/core/auth-storage.ts";
|
||||
|
|
@ -938,6 +938,63 @@ describe("ModelRegistry", () => {
|
|||
expect(registry.getProviderDisplayName("oauth-provider")).toBe("OAuth Provider");
|
||||
});
|
||||
|
||||
test("modelOverrides apply to dynamically registered provider models", async () => {
|
||||
writeRawModelsJson({
|
||||
"extension-provider": {
|
||||
modelOverrides: {
|
||||
"extension-model": {
|
||||
name: "Overridden Extension Model",
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
xhigh: "max",
|
||||
},
|
||||
headers: { "x-model-override": "enabled" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
|
||||
registry.registerProvider("extension-provider", {
|
||||
baseUrl: "https://provider.test/v1",
|
||||
apiKey: "test-key",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "extension-model",
|
||||
name: "Extension Model",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const model = registry.find("extension-provider", "extension-model");
|
||||
expect(model).toBeDefined();
|
||||
if (!model) {
|
||||
throw new Error("extension model was not registered");
|
||||
}
|
||||
expect(model.name).toBe("Overridden Extension Model");
|
||||
expect(model.thinkingLevelMap).toEqual({
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
xhigh: "max",
|
||||
});
|
||||
expect(getSupportedThinkingLevels(model)).toEqual(["high", "xhigh"]);
|
||||
expect(await registry.getApiKeyAndHeaders(model)).toMatchObject({
|
||||
ok: true,
|
||||
headers: { "x-model-override": "enabled" },
|
||||
});
|
||||
});
|
||||
|
||||
test("stored API key env propagates to request auth and resolves headers", async () => {
|
||||
authStorage.set("cloudflare-ai-gateway", {
|
||||
type: "api_key",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue