4c4b83bb68
Self-contained SSE-based provider layer for OpenAI-compatible endpoints (Ollama/llama.cpp/LM Studio/vLLM), Anthropic, Gemini, and OpenRouter. - budget.ts: re-export core token-budget engine (countTokens, allocateBudget, DEFAULT_BUDGET) - client.ts: streamCompletion/collectStream/complete with budget enforcement via enforceBudget + measureBudget - discover.ts: auto-detect local providers by probing common /v1/models - adapters.ts: per-provider param mapping + streaming adapters + factory - thinking.ts: thinking/reasoning-param normalization per provider - http.ts: SSE parsing + 429 retry - errors.ts: BudgetExceededError 35 unit/integration tests pass; typecheck and lint clean.
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
/**
|
|
* Thinking/reasoning parameter mapping.
|
|
*
|
|
* Different providers express the reasoning budget differently:
|
|
*
|
|
* - OpenAI-style (`openai-compatible`, `openrouter`): `reasoning_effort`
|
|
* (`"low" | "medium" | "high"`), optionally `max_tokens` for the reasoning
|
|
* budget.
|
|
* - Anthropic: a `thinking` object `{ enabled: boolean, budget_tokens: number }`
|
|
* plus `thinking_budget` (must be <= 32768 and > 0).
|
|
* - Google (Gemini): `reasoning_effort` + `thinking_budget`.
|
|
*
|
|
* This module normalizes the provider-agnostic `ThinkingConfig` into the
|
|
* provider-specific wire format. See CHUNK#8.md §3.
|
|
*/
|
|
|
|
import type { ProviderKind, ThinkingConfig } from "./providers/types.js";
|
|
|
|
/** The maximum reasoning budget Anthropic accepts. */
|
|
const ANTHROPIC_MAX_THINKING_TOKENS = 32768;
|
|
|
|
/** Normalize a thinking config into the wire-format body fragment for a kind. */
|
|
export function normalizeThinking(
|
|
kind: ProviderKind,
|
|
thinking?: ThinkingConfig,
|
|
): Record<string, unknown> {
|
|
if (!thinking?.enabled) {
|
|
return {};
|
|
}
|
|
|
|
switch (kind) {
|
|
case "anthropic":
|
|
return normalizeAnthropicThinking(thinking);
|
|
case "google":
|
|
return normalizeGoogleThinking(thinking);
|
|
case "openai-compatible":
|
|
case "openrouter":
|
|
default:
|
|
return normalizeOpenAIThinking(thinking);
|
|
}
|
|
}
|
|
|
|
/** OpenAI-style: `reasoning_effort` + optional `max_tokens`. */
|
|
function normalizeOpenAIThinking(thinking: ThinkingConfig): Record<string, unknown> {
|
|
const effort = thinking.effort ?? "medium";
|
|
const body: Record<string, unknown> = {
|
|
reasoning_effort: effort,
|
|
};
|
|
if (typeof thinking.budget === "number") {
|
|
body.max_tokens = thinking.budget;
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/** Anthropic: `thinking` object + `thinking_budget`. */
|
|
function normalizeAnthropicThinking(thinking: ThinkingConfig): Record<string, unknown> {
|
|
const budget =
|
|
typeof thinking.budget === "number"
|
|
? Math.min(thinking.budget, ANTHROPIC_MAX_THINKING_TOKENS)
|
|
: 2048;
|
|
return {
|
|
thinking: {
|
|
enabled: true,
|
|
budget_tokens: budget,
|
|
},
|
|
thinking_budget: budget,
|
|
};
|
|
}
|
|
|
|
/** Google: `reasoning_effort` + `thinking_budget`. */
|
|
function normalizeGoogleThinking(thinking: ThinkingConfig): Record<string, unknown> {
|
|
const effort = thinking.effort ?? "medium";
|
|
const budget = typeof thinking.budget === "number" ? thinking.budget : 2048;
|
|
return {
|
|
reasoning_effort: effort,
|
|
thinking_budget: budget,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compute the reasoning budget (in tokens) to advertise to the budget engine,
|
|
* so the output budget accounts for the reasoning spend. Returns 0 when
|
|
* thinking is disabled.
|
|
*/
|
|
export function reasoningBudgetTokens(thinking?: ThinkingConfig): number {
|
|
if (!thinking?.enabled) {
|
|
return 0;
|
|
}
|
|
return typeof thinking.budget === "number" ? thinking.budget : 2048;
|
|
}
|