feat(ai): add packages/ai provider layer (Chunk #8)
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.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user