mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai): add per-thinking-level sampling parameters (#9776)
This commit is contained in:
parent
70eceaade6
commit
76dfb88f63
12 changed files with 250 additions and 31 deletions
|
|
@ -182,6 +182,7 @@
|
|||
- Added a Built-in section in `pi config` to disable the built-in `mcp`, `llama.cpp`, `codemode`, and `tool-search` extensions globally or per project, stored as `-builtin:<name>` in the `extensions` setting. SDK inline extensions opt in with `builtin: true`.
|
||||
- Added `+name` and `-name` entries to the `defaultTools` setting to add or remove tools without repeating the defaults, for example `"defaultTools": ["+codemode"]`. Project entries of this form apply on top of the user setting. Documented how to enable `codemode` without MCP and how to use classifier models such as Jev from codemode scripts.
|
||||
- Added the token usage and cost of codemode `models.classify()` calls to the codemode tool result, so they count toward the session cost; the codemode result shows each call's cost.
|
||||
- Added `samplingParamsByThinkingLevel` to `models.json` for per-thinking-level sampling parameter overrides.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,32 @@ Use `promptCache` to declare the provider's best-effort cache lifetime in second
|
|||
|
||||
Choose the conservative end of any published range. A model without a lifetime for the active tier is not eligible for cache warming. A `modelOverrides` entry can set `inputLimits` or `promptCache` for a built-in or extension model, including a model accessed through a validated proxy. See [`cacheWarming`](settings.md#model-and-thinking).
|
||||
|
||||
### Configure sampling by thinking level
|
||||
|
||||
OpenAI-compatible APIs support free-form `samplingParams` model defaults and `samplingParamsByThinkingLevel` overrides. The latter uses Pi thinking-level keys (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`), not provider values from `thinkingLevelMap`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "qwen-thinking-model",
|
||||
"reasoning": true,
|
||||
"samplingParams": {
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.95
|
||||
},
|
||||
"samplingParamsByThinkingLevel": {
|
||||
"off": {
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.8
|
||||
},
|
||||
"high": {
|
||||
"top_k": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pi first clamps unsupported thinking levels, then merges model `samplingParams`, the effective level's override, and request-level `samplingParams` in that order. Later values win per key. Missing levels inherit the model defaults. `modelOverrides` merges per-level entries per key with the base model. These fields apply only to `openai-completions`, `openai-responses`, and `azure-openai-responses`; other APIs ignore them.
|
||||
|
||||
Compatibility settings should describe verified differences in the endpoint's request or response behavior. Do not enable them based only on an endpoint advertising OpenAI or Anthropic compatibility.
|
||||
|
||||
## Use classifier models
|
||||
|
|
|
|||
|
|
@ -63,6 +63,17 @@ const ThinkingLevelMapSchema = Type.Object({
|
|||
max: Type.Optional(ThinkingLevelMapValueSchema),
|
||||
});
|
||||
|
||||
const SamplingParamsSchema = Type.Record(Type.String(), Type.Unknown());
|
||||
const SamplingParamsByThinkingLevelSchema = Type.Object({
|
||||
off: Type.Optional(SamplingParamsSchema),
|
||||
minimal: Type.Optional(SamplingParamsSchema),
|
||||
low: Type.Optional(SamplingParamsSchema),
|
||||
medium: Type.Optional(SamplingParamsSchema),
|
||||
high: Type.Optional(SamplingParamsSchema),
|
||||
xhigh: Type.Optional(SamplingParamsSchema),
|
||||
max: Type.Optional(SamplingParamsSchema),
|
||||
});
|
||||
|
||||
const ChatTemplateKwargScalarSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]);
|
||||
const ChatTemplateKwargVariableSchema = Type.Object({
|
||||
$var: Type.Union([Type.Literal("thinking.enabled"), Type.Literal("thinking.effort")]),
|
||||
|
|
@ -198,7 +209,8 @@ const ModelDefinitionSchema = Type.Object({
|
|||
promptCache: Type.Optional(ModelPromptCacheSchema),
|
||||
contextWindow: Type.Optional(Type.Number()),
|
||||
maxTokens: Type.Optional(Type.Number()),
|
||||
samplingParams: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
||||
samplingParams: Type.Optional(SamplingParamsSchema),
|
||||
samplingParamsByThinkingLevel: Type.Optional(SamplingParamsByThinkingLevelSchema),
|
||||
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||
compat: Type.Optional(ProviderCompatSchema),
|
||||
});
|
||||
|
|
@ -221,7 +233,8 @@ const ModelOverrideSchema = Type.Object({
|
|||
promptCache: Type.Optional(ModelPromptCacheSchema),
|
||||
contextWindow: Type.Optional(Type.Number()),
|
||||
maxTokens: Type.Optional(Type.Number()),
|
||||
samplingParams: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
||||
samplingParams: Type.Optional(SamplingParamsSchema),
|
||||
samplingParamsByThinkingLevel: Type.Optional(SamplingParamsByThinkingLevelSchema),
|
||||
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||
compat: Type.Optional(ProviderCompatSchema),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ export interface ProviderChatModelConfig extends ProviderModelConfigBase {
|
|||
promptCache?: Model<Api>["promptCache"];
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
samplingParams?: Record<string, unknown>;
|
||||
samplingParams?: Model<Api>["samplingParams"];
|
||||
samplingParamsByThinkingLevel?: Model<Api>["samplingParamsByThinkingLevel"];
|
||||
compat?: Model<Api>["compat"];
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +161,19 @@ function mergeInputLimits(
|
|||
};
|
||||
}
|
||||
|
||||
function mergeSamplingParamsByThinkingLevel(
|
||||
base: Model<Api>["samplingParamsByThinkingLevel"],
|
||||
override: Model<Api>["samplingParamsByThinkingLevel"],
|
||||
): Model<Api>["samplingParamsByThinkingLevel"] {
|
||||
if (!override) return base;
|
||||
const merged = { ...base };
|
||||
for (const level of ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const) {
|
||||
const params = override[level];
|
||||
if (params) merged[level] = { ...base?.[level], ...params };
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function applyModelOverride(model: Model<Api>, override: ModelsJsonModelOverride): Model<Api> {
|
||||
return {
|
||||
...model,
|
||||
|
|
@ -185,6 +199,10 @@ function applyModelOverride(model: Model<Api>, override: ModelsJsonModelOverride
|
|||
samplingParams: override.samplingParams
|
||||
? { ...model.samplingParams, ...override.samplingParams }
|
||||
: model.samplingParams,
|
||||
samplingParamsByThinkingLevel: mergeSamplingParamsByThinkingLevel(
|
||||
model.samplingParamsByThinkingLevel,
|
||||
override.samplingParamsByThinkingLevel,
|
||||
),
|
||||
compat: mergeCompat(model.compat, override.compat),
|
||||
};
|
||||
}
|
||||
|
|
@ -224,6 +242,7 @@ function modelFromJson(
|
|||
contextWindow: definition.contextWindow ?? 128000,
|
||||
maxTokens: definition.maxTokens ?? 16384,
|
||||
samplingParams: definition.samplingParams,
|
||||
samplingParamsByThinkingLevel: definition.samplingParamsByThinkingLevel,
|
||||
headers: undefined,
|
||||
compat: mergeCompat(providerConfig.compat, definition.compat),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -754,7 +754,7 @@ describe("ModelRegistry", () => {
|
|||
expect(compat?.allowedFallbackModels).toEqual([]);
|
||||
});
|
||||
|
||||
test("custom model and model override carry sampling params", async () => {
|
||||
test("custom models and model overrides carry sampling params", async () => {
|
||||
writeRawModelsJson({
|
||||
openrouter: {
|
||||
baseUrl: "https://my-proxy.example.com/v1",
|
||||
|
|
@ -763,11 +763,22 @@ describe("ModelRegistry", () => {
|
|||
{
|
||||
id: "custom/sampling-model",
|
||||
samplingParams: { temperature: 1, top_p: 0.95, top_k: 0 },
|
||||
samplingParamsByThinkingLevel: {
|
||||
low: { temperature: 0.6, top_p: 0.95 },
|
||||
high: { temperature: 0.8 },
|
||||
},
|
||||
},
|
||||
],
|
||||
modelOverrides: {
|
||||
"custom/sampling-model": {
|
||||
samplingParamsByThinkingLevel: {
|
||||
low: { temperature: 0.5, top_k: 20 },
|
||||
max: { temperature: 1 },
|
||||
},
|
||||
},
|
||||
"anthropic/claude-sonnet-4": {
|
||||
samplingParams: { top_p: 0.9 },
|
||||
samplingParamsByThinkingLevel: { high: { temperature: 0.8 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -778,13 +789,20 @@ describe("ModelRegistry", () => {
|
|||
|
||||
const custom = models.find((m) => m.id === "custom/sampling-model");
|
||||
expect(custom?.samplingParams).toEqual({ temperature: 1, top_p: 0.95, top_k: 0 });
|
||||
expect(custom?.samplingParamsByThinkingLevel).toEqual({
|
||||
low: { temperature: 0.5, top_p: 0.95, top_k: 20 },
|
||||
high: { temperature: 0.8 },
|
||||
max: { temperature: 1 },
|
||||
});
|
||||
|
||||
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
|
||||
expect(sonnet?.samplingParams).toEqual({ top_p: 0.9 });
|
||||
expect(sonnet?.samplingParamsByThinkingLevel).toEqual({ high: { temperature: 0.8 } });
|
||||
|
||||
// Models without sampling config keep it unset.
|
||||
const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1");
|
||||
expect(opus?.samplingParams).toBeUndefined();
|
||||
expect(opus?.samplingParamsByThinkingLevel).toBeUndefined();
|
||||
});
|
||||
|
||||
test("custom model and model override carry prompt cache lifetimes", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue