import { ModelPricing, LLMPort, EmbeddingsPort, ValidationStrategy, OnRetry, KnownModelConstraint } from '@llm-ports/core'; import { OpenAI } from 'openai'; /** * Behavioral model fingerprinting (alpha.24+). * * Problem: maintaining a static catalog of (model × provider) → capability * mappings is unsustainable. Three CoT field conventions exist across the * OpenAI-compat ecosystem (`reasoning`, `reasoning_content`, inline-``), * provider naming drifts, and new reasoning models ship weekly. The empirical * survey at docs/research/reasoning-models-survey-2026-06.md catalogues ~30+ * reasoning models across 5 providers; every regex entry is one more piece of * code that goes stale on its own schedule. * * Solution: behavioral fingerprinting. At first contact with a model, fire * one small probe call ("what's 2+2") and inspect the response shape: * * - `message.reasoning` populated → Cerebras-style reasoning model * - `message.reasoning_content` populated → vLLM-style reasoning model * - `usage.completion_tokens_details.reasoning_tokens > 0` → OpenAI-native * reasoning model * - inline `...` markers in `message.content` → legacy R1 * style * * Cache the result by (baseURL, modelId). Next process startup reads the * cache and skips the probe entirely. The catalog stays as a cheap shortcut * for the well-known cases (OpenAI o-series, gpt-5-nano); for everything * else, the fingerprint cache is the answer. * * This file ships the cache backend interface, two default backends * (in-memory + file), the `fingerprintModel` standalone helper, and the * `inspectResponseForFingerprint` analyzer the adapter uses for free * fingerprinting on existing inflight responses (no probe call needed when * the model already produced a fingerprint-able response). */ /** * The fingerprint we cache per (baseURL, modelId) tuple. Captures enough * information for the adapter to skip first-call discovery. */ interface ModelFingerprint { /** Canonical model id (after normalizeModelId — strip provider prefix). */ modelId: string; /** Provider baseURL (or "openai-native" sentinel for OpenAI native). */ baseURL: string; /** True if the model produces hidden chain-of-thought tokens. */ reasoningModel: boolean; /** * Which field exposes the chain-of-thought, when reasoningModel is true. * - "reasoning" → message.reasoning (Cerebras, Groq, SambaNova) * - "reasoning_content" → message.reasoning_content (DeepInfra, Parasail) * - "reasoning_tokens" → usage.completion_tokens_details.reasoning_tokens * (OpenAI native; no separate text field) * - "inline-think" → ... embedded in message.content * (legacy R1 distills on some providers) * - undefined → not a reasoning model OR couldn't determine */ reasoningField?: "reasoning" | "reasoning_content" | "reasoning_tokens" | "inline-think"; /** ISO timestamp when this fingerprint was captured. */ fingerprintedAt: string; /** * Schema version. Bumped when the fingerprint shape changes so old caches * are invalidated gracefully instead of crashing the loader. */ schemaVersion: 1; } /** * Cache backend interface. Implementations are responsible for persistence * (file, Redis, S3, etc.). All methods may return synchronously or as a * Promise; the adapter awaits regardless. */ interface FingerprintCacheBackend { /** Return cached fingerprint for the key, or null when absent. */ get(key: string): Promise | ModelFingerprint | null; /** Persist the fingerprint under the key. */ set(key: string, value: ModelFingerprint): Promise | void; /** Optional: delete a single entry. Used by test helpers and admin tools. */ delete?(key: string): Promise | void; } /** * Simple in-memory cache. Lifetime is the current process. Useful for * development, tests, and short-lived workers where re-fingerprinting on * restart is cheap. */ declare class InMemoryFingerprintCache implements FingerprintCacheBackend { private readonly store; get(key: string): ModelFingerprint | null; set(key: string, value: ModelFingerprint): void; delete(key: string): void; /** Test-only: clear all entries. */ _clear(): void; /** Test-only: snapshot for inspection. */ _snapshot(): ReadonlyMap; } /** * File-backed cache. Stores fingerprints as JSON at the configured path. * Loads lazily on first `get` and writes back on every `set` (atomic via * temp + rename). Suitable for long-running workers and CI warm-starts. * * NOT suitable for concurrent multi-process writers; if you need that, * supply your own backend (Redis, S3, etc.) that handles the locking. */ declare class FileFingerprintCache implements FingerprintCacheBackend { readonly path: string; private cache; private loadPromise; constructor(path: string); private ensureLoaded; get(key: string): Promise; set(key: string, value: ModelFingerprint): Promise; delete(key: string): Promise; private persist; } /** * Build the cache key for a (baseURL, modelId) tuple. Normalizes the * baseURL by stripping trailing slashes and the modelId by stripping the * provider namespace prefix (alpha.22 normalization). * * Examples: * buildFingerprintKey(undefined, "gpt-5") * → "openai-native::gpt-5" * buildFingerprintKey("https://api.deepinfra.com/v1/openai", "openai/gpt-oss-120b") * → "https://api.deepinfra.com/v1/openai::gpt-oss-120b" * buildFingerprintKey("https://api.cerebras.ai/v1/", "gpt-oss-120b") * → "https://api.cerebras.ai/v1::gpt-oss-120b" */ declare function buildFingerprintKey(baseURL: string | undefined, modelId: string): string; /** * Inspect a chat-completion response and derive a fingerprint. Used by the * adapter to fingerprint for free on every inflight response — no separate * probe call needed when the model already produced something we can read. * * Returns a partial fingerprint (the shape-derivable fields). The caller is * responsible for adding modelId, baseURL, fingerprintedAt, schemaVersion. * * Detection priority (first match wins): * 1. `usage.completion_tokens_details.reasoning_tokens > 0` → * reasoningModel=true, reasoningField="reasoning_tokens" * (OpenAI native; the reasoning tokens are billed but not text-exposed) * 2. `message.reasoning_content` populated string → * reasoningModel=true, reasoningField="reasoning_content" * (vLLM-style: DeepInfra, Parasail) * 3. `message.reasoning` populated string → * reasoningModel=true, reasoningField="reasoning" * (Cerebras-style: Cerebras, Groq, SambaNova) * 4. `message.content` contains `...` markers → * reasoningModel=true, reasoningField="inline-think" * (legacy R1 distills emitted raw) * 5. Otherwise: reasoningModel=false (no signal observed) * * A "no signal" verdict is NOT cached — it could be a non-reasoning prompt * to a reasoning model. The caller decides whether to cache negatives. */ declare function inspectResponseForFingerprint(response: unknown): Pick; /** * Fire one small probe call against the given client + modelId and produce * a fingerprint. Useful for CI warm-start scripts that want to populate the * cache before production traffic starts. * * @param client An initialized OpenAI client. * @param modelId Provider-native model id (raw; will be normalized internally). * @param opts Optional baseURL override (recorded in the key). If * undefined, the client's own baseURL is read; if both are * absent, the "openai-native" sentinel is used. */ declare function fingerprintModel(client: OpenAI, modelId: string, opts?: { baseURL?: string; }): Promise; /** * OpenAI adapter implementing LLMPort + EmbeddingsPort. * * Wraps the openai npm package's chat completions and embeddings APIs. * The same adapter serves OpenAI plus 10+ OpenAI-compatible providers * via the `baseURL` option (Azure OpenAI, Groq, Together AI, Fireworks AI, * DeepInfra, Perplexity, Cerebras, LiteLLM proxy, Ollama compat-mode, etc.). */ interface OpenAIAdapterOptions { apiKey: string; /** * Override the API base URL. Use this for OpenAI-compatible providers: * - Azure OpenAI: `https://.openai.azure.com/openai/deployments/` * - Groq: `https://api.groq.com/openai/v1` * - Together AI: `https://api.together.xyz/v1` * - Fireworks AI: `https://api.fireworks.ai/inference/v1` * - DeepInfra: `https://api.deepinfra.com/v1/openai` * - Perplexity: `https://api.perplexity.ai` * - Cerebras: `https://api.cerebras.ai/v1` * - LiteLLM proxy: self-hosted, e.g. `http://localhost:4000` * - Ollama compat-mode: `http://localhost:11434/v1` (prefer adapter-ollama) */ baseURL?: string; /** Inject a custom fetch (used for tests / proxies). */ fetch?: typeof fetch; /** Default validation strategy if the registry doesn't override per-call. */ validationStrategy?: ValidationStrategy; /** Override pricing for any model id. Falls back to bundled OPENAI_PRICING. */ pricingOverrides?: Record; /** * Friendly name for the adapter to use in error messages and providerAlias * default. Useful when you point this adapter at a non-OpenAI baseURL and * want errors to say "groq" instead of "openai". The adapter token in env * config still says "openai" because that's the SDK shape. */ displayName?: string; /** * Number of retries the OpenAI SDK performs internally for retriable HTTP * errors (408, 409, 429, 500+). Defaults to 2 (the SDK's own default). The * SDK does NOT retry 401s; that's handled separately in this adapter — see * {@link OpenAIAdapterOptions.transientAuthRetries}. */ maxRetries?: number; /** * Number of retries to attempt on transient 401 responses. OpenAI project * keys (sk-proj-*) have burst-protection that occasionally returns * 401 "Incorrect API key" when too many requests arrive in a short window, * even though the key is valid. The adapter only retries 401s if a prior * request on this same client previously succeeded — that's how it * distinguishes a transient burst-protection 401 from a real auth failure. * Defaults to 2 retries with exponential backoff (500ms, 1500ms). * Set to 0 to disable. */ transientAuthRetries?: number; /** * Override the backoff delay between transient-401 retries. Receives the * 0-indexed retry attempt (0 = first retry) and returns the delay in * milliseconds. Default is `(attempt) => 500 * Math.pow(3, attempt)` — * 500ms, 1500ms, 4500ms... Tests inject `() => 0` to skip the wait. */ transientAuthBackoffMs?: (attempt: number) => number; /** * Maximum bytes per base64 image. Defaults to 20MB (OpenAI's per-image * limit). Set to 0 or a negative number to disable size validation. */ imageSizeLimitBytes?: number; /** * Set to `true` to allow the OpenAI SDK to run in a browser environment. * The SDK refuses by default to prevent accidental exposure of API keys. * * Only enable this when you understand the risk and have a mitigation in * place: a server-side proxy that strips keys from the request before * forwarding, a "bring your own API key" UI where users supply their own * key, or an internal tool exposed only to trusted users. Forwarded to * `new OpenAI({ dangerouslyAllowBrowser })` verbatim. * * Available since `0.1.0-alpha.9`. */ dangerouslyAllowBrowser?: boolean; /** * Use OpenAI-style strict `response_format: { type: "json_schema", strict: true }` * for `generateStructured` instead of classic `response_format: { type: "json_object" }`. * With strict mode the provider constrains decoding to the exact schema * before tokens are produced, so invalid JSON or missing fields are * impossible (modulo provider bugs). The Zod schema is converted to * JSON Schema via `zod-to-json-schema`, then post-processed to add * `additionalProperties: false` on every object (a hard requirement of * OpenAI / Cerebras / Groq strict mode). * * Defaults to auto-detect (alpha.14+). Auto-enabled when: * - `baseURL` is unset (= OpenAI native; strict json_schema has been GA * on gpt-4o / gpt-5 / o-series since August 2024) * - `baseURL` contains `api.cerebras.ai` (Cerebras's gpt-oss / Qwen3.6 * endpoints silently ignore classic `json_object` mode; strict mode * is required for reliable structured output) * - `baseURL` contains `api.groq.com` (verified to support strict * `response_format: json_schema` with constrained decoding) * - `baseURL` contains `api.sambanova.ai` (added alpha.15+; * empirically verified — MiniMax-M2.7 jumped from 0/10 → 10/10 on * nested schemas with strict mode forced on) * * Stays OPT-IN (default `false`) for unverified compat providers like * Together AI, Fireworks AI, Clarifai. Set `useStrictResponseFormat: true` * explicitly once you've verified the provider's strict-mode support. * * Opt-out: set `useStrictResponseFormat: false` explicitly if your Zod * schemas use open shapes that can't accept `additionalProperties: false` * (e.g. `z.record(...)`, schemas with computed/optional fields the * model is allowed to extend), or if strict mode is causing your model * to reject the request. * * Available since `0.1.0-alpha.9`; default expanded to OpenAI native + * Groq in `0.1.0-alpha.14`; SambaNova added `0.1.0-alpha.15`. */ useStrictResponseFormat?: boolean; /** * Observability hook fired whenever the adapter retries an in-flight * request for a known transient reason. Sync or async; called * fire-and-forget. Throwing from the hook does NOT cancel the retry. * Fires for: transient-auth (project-key burst-protection 401), * capability-fallback (temperature/json_object/system-message rejection), * reasoning-starvation (model used full budget on hidden reasoning), * validation-feedback (structured output failed schema; retry with feedback). */ onRetry?: OnRetry; /** * Streamed cost surfacing (alpha.25+). When `true` (default), the adapter * adds `stream_options: { include_usage: true }` to streaming requests so * the provider returns a final chunk with usage counts, which the adapter * uses to compute cost and fire the Registry's stream-complete callback. * * Set to `false` when the underlying compat provider rejects the * `stream_options` field. The stream itself still works; only the * post-completion `onCost` / `onTokenUsage` events are suppressed for * that provider (matches alpha.24 behavior). */ streamUsage?: boolean; /** * Behavioral fingerprint cache (alpha.24+). When supplied, the adapter * seeds the capability learner from this cache before each call and * writes back the observed fingerprint after each successful call. This * skips the first-call discovery penalty for known models AND avoids * the static catalog being load-bearing for novel reasoning models. * * The cache is keyed by `(baseURL, modelId)`. Different providers serving * the same canonical model get separate entries (correctly — they may * expose different response shapes for the same weights). The same model * served by the same provider across multiple processes shares state when * the cache backend persists. * * Two bundled backends: * - `InMemoryFingerprintCache` — Map; lifetime is the current process. * Useful for dev, tests, short workers. * - `FileFingerprintCache(path)` — atomic JSON file. Useful for * long-running workers and CI warm-starts. * * Bring your own backend (Redis, S3, etc.) by implementing * `FingerprintCacheBackend`. * * Default: undefined (no cache). Static catalog + runtime detection * (alpha.22) handle correctness without fingerprinting; the fingerprint * cache is purely an optimization to skip the first-call penalty. */ fingerprintCache?: FingerprintCacheBackend; } /** * Auto-detect whether to default `useStrictResponseFormat` to true based on * the `baseURL`. See the option's docstring for the rationale per provider. * * Exported for testability and for users who want to reuse the same default * logic when constructing multiple adapter instances programmatically. */ declare function autoDetectStrictResponseFormat(baseURL: string | undefined): boolean; interface OpenAIAdapter { name: "openai"; pricing: Record; createLLMPort: (modelId: string, alias: string) => LLMPort; createEmbeddingsPort: (modelId: string, alias: string) => EmbeddingsPort; } declare function createOpenAIAdapter(opts: OpenAIAdapterOptions): OpenAIAdapter; /** * OpenAI model pricing (USD per 1M tokens). * * Source: https://openai.com/api/pricing/ * Last verified: 2026-06-18 by @baabakk * * Update process: edit this file, bump the "Last verified" date, open a PR * with the source URL referenced. Changeset patch bump on adapter-openai. * * Users can override these via the registry's `pricingOverrides` option, * which is the daily-use escape hatch when prices change between releases * or when an enterprise has negotiated rates. * * Note: This same adapter serves OpenAI-compatible providers (Groq, Together * AI, Fireworks, DeepInfra, Perplexity, Cerebras, LiteLLM proxy, etc.) via * the `baseURL` option. Those providers have their own pricing — supply it * via `pricingOverrides`, OR rely on the curated bundled entries below for * the compat models the team has actively verified (alpha.21+ section). */ declare const OPENAI_PRICING: Record; declare function lookupOpenAIPricing(modelId: string): ModelPricing | undefined; /** * Runtime capability discovery for OpenAI-shaped APIs. * * OpenAI does not expose programmatic capability discovery (no API endpoint * tells you "this model rejects custom temperature" or "this model doesn't * support response_format: json_object"). Hardcoded capability tables go * stale every time a new model ships. * * Strategy: catch the specific OpenAI error codes that signal a constraint, * learn the constraint, remember it for the rest of the process. Subsequent * calls don't re-discover; they apply the learned constraint up front. * * Constraints we discover: * - temperatureLocked: model rejects custom `temperature` value * - jsonModeUnsupported: model rejects `response_format: { type: "json_object" }` * - systemMessageInUserOnly: model rejects a separate `system` message * * Users who already know their model's constraints can supply them via * `ModelCapabilities` in pricingOverrides — that takes precedence over * discovery (no first-call learning round-trip). * * The learner instance + Map machinery is shared across all adapters via * `createCapabilityLearner` from `@llm-ports/core`. This file contributes * the OpenAI-specific error classifiers. */ /** * Strip a provider/namespace prefix from a model ID, returning the * canonical name. The canonical name is the substring after the last `/`. * Model IDs with no `/` pass through unchanged. * * Examples: * gpt-oss-120b → gpt-oss-120b (OpenAI native, unchanged) * openai/gpt-oss-120b → gpt-oss-120b (DeepInfra/Groq form) * deepseek-ai/DeepSeek-V4-Flash → DeepSeek-V4-Flash * XiaomiMiMo/MiMo-V2.5 → MiMo-V2.5 * google/gemma-4-31B-it → gemma-4-31B-it * models/gemini-2.0-flash → gemini-2.0-flash */ declare function normalizeModelId(modelId: string): string; /** * **OPTIMIZATION SHORTCUT — NOT load-bearing for correctness (alpha.24+).** * * Pre-seeds the learner so the first call against a catalog-matched model * skips the discovery round-trip (reasoning starvation + learn-and-retry). * Without this catalog, the very first call to a reasoning model pays one * wasted round-trip; subsequent calls in the same process are fine. * * **The catalog is a perf shortcut, NOT the correctness path.** Runtime * detection (alpha.22+) catches every reasoning model by inspecting: * - `usage.completion_tokens_details.reasoning_tokens` (OpenAI native) * - `choices[0].message.reasoning` (Cerebras-style: Cerebras, Groq, SambaNova) * - `choices[0].message.reasoning_content` (vLLM-style: DeepInfra, Parasail) * - inline `...` in `choices[0].message.content` (legacy R1) * * Behavioral fingerprinting (alpha.24+) lets users skip even the first-call * penalty by caching observed shapes across processes — see * `FingerprintCacheBackend` in fingerprint.ts and the * `createOpenAIAdapter({ fingerprintCache })` option. * * **The catalog is FROZEN.** New reasoning models discovered in production * are NOT added here. New entries would be one more piece of code that * goes stale on its own schedule. The empirical survey at * `docs/research/reasoning-models-survey-2026-06.md` enumerated ~30+ * reasoning models across 5 OpenAI-compat providers — maintaining regex * entries for all of them is exactly the unsustainable burden the * fingerprint cache solves. * * **Existing entries stay.** The well-known cases below (OpenAI o-series, * gpt-5-nano, gpt-oss family, Qwen3.6, MiniMax-M2.7, Xiaomi MiMo) are * production-grade and remove a pointless round-trip for the common case. * They're cheap to keep and their patterns are stable. * * **For all the cases below + everything else:** runtime detection * (correctness) + fingerprint cache (optimization) is the architecture. * See `docs/concepts/capability-detection.md` for the three-tier design. * * Patterns are case-insensitive and tolerate underscore-vs-hyphen + * dot-vs-underscore variation, since OpenAI-compat providers normalize * model IDs inconsistently (Clarifai uses `Qwen3_6`, others use `qwen-3.6`). * * Extend this list as new reasoning models ship behind OpenAI-compat * baseURLs (Clarifai, SambaNova, Groq, Together AI, Fireworks, Cerebras, * Perplexity, DeepInfra, LiteLLM proxy, etc.). */ declare const KNOWN_REASONING_MODELS: readonly KnownModelConstraint[]; /** * Convert llm-ports ContentBlock[] to/from OpenAI's chat completions format. * * OpenAI message structure: * - User/system: content can be `string` or array of typed parts * - { type: "text", text: string } * - { type: "image_url", image_url: { url: string, detail?: "auto"|"low"|"high" } } * - { type: "input_audio", input_audio: { data, format: "wav"|"mp3" } } * - Assistant: content + tool_calls separately * - tool_calls: [{ id, type: "function", function: { name, arguments: JSON-string } }] * - Tool: { role: "tool", tool_call_id, content } * * Notable differences from Anthropic: * - Tool calls live in `tool_calls` field, not as content blocks * - Tool results are separate messages (role: "tool"), not blocks * - Image URLs use "image_url" wrapper; base64 encoded as data URI */ interface OpenAIToolCall { id: string; type: "function"; function: { name: string; arguments: string; }; } /** * Best-effort parse of one or more tool calls out of a harmony-formatted * reasoning_content string. Returns null when no parseable harmony tool * call is found (so the caller can fall through to corrective rescue). */ declare function parseHarmonyToolCalls(reasoningContent: string | null | undefined): OpenAIToolCall[] | null; export { FileFingerprintCache, type FingerprintCacheBackend, InMemoryFingerprintCache, KNOWN_REASONING_MODELS, type ModelFingerprint, OPENAI_PRICING, type OpenAIAdapter, type OpenAIAdapterOptions, autoDetectStrictResponseFormat, buildFingerprintKey, createOpenAIAdapter, fingerprintModel, inspectResponseForFingerprint, lookupOpenAIPricing, normalizeModelId, parseHarmonyToolCalls };