@providerprotocol/ai 0.0.20 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/anthropic/index.d.ts +184 -14
  2. package/dist/anthropic/index.js +306 -107
  3. package/dist/anthropic/index.js.map +1 -1
  4. package/dist/{chunk-P5IRTEM5.js → chunk-7WYBJPJJ.js} +2 -2
  5. package/dist/chunk-I2VHCGQE.js +49 -0
  6. package/dist/chunk-I2VHCGQE.js.map +1 -0
  7. package/dist/{chunk-UMKWXGO3.js → chunk-M4BMM5IB.js} +86 -2
  8. package/dist/chunk-M4BMM5IB.js.map +1 -0
  9. package/dist/{chunk-SKY2JLA7.js → chunk-MKDLXV4O.js} +1 -1
  10. package/dist/chunk-MKDLXV4O.js.map +1 -0
  11. package/dist/{chunk-Z7RBRCRN.js → chunk-NWS5IKNR.js} +37 -11
  12. package/dist/chunk-NWS5IKNR.js.map +1 -0
  13. package/dist/{chunk-U3FZWV4U.js → chunk-RFWLEFAB.js} +100 -43
  14. package/dist/chunk-RFWLEFAB.js.map +1 -0
  15. package/dist/{chunk-U4JJC2YX.js → chunk-RS7C25LS.js} +36 -11
  16. package/dist/chunk-RS7C25LS.js.map +1 -0
  17. package/dist/google/index.d.ts +35 -24
  18. package/dist/google/index.js +273 -99
  19. package/dist/google/index.js.map +1 -1
  20. package/dist/http/index.d.ts +3 -3
  21. package/dist/http/index.js +4 -4
  22. package/dist/index.d.ts +103 -38
  23. package/dist/index.js +346 -153
  24. package/dist/index.js.map +1 -1
  25. package/dist/ollama/index.d.ts +14 -16
  26. package/dist/ollama/index.js +68 -16
  27. package/dist/ollama/index.js.map +1 -1
  28. package/dist/openai/index.d.ts +25 -133
  29. package/dist/openai/index.js +208 -122
  30. package/dist/openai/index.js.map +1 -1
  31. package/dist/openrouter/index.d.ts +28 -53
  32. package/dist/openrouter/index.js +179 -72
  33. package/dist/openrouter/index.js.map +1 -1
  34. package/dist/provider-DWEAzeM5.d.ts +1329 -0
  35. package/dist/proxy/index.d.ts +2 -3
  36. package/dist/proxy/index.js +174 -17
  37. package/dist/proxy/index.js.map +1 -1
  38. package/dist/{retry-DR7YRJDz.d.ts → retry-DmPmqZL6.d.ts} +12 -3
  39. package/dist/{stream-DRHy6q1a.d.ts → stream-DbkLOIbJ.d.ts} +15 -5
  40. package/dist/xai/index.d.ts +16 -88
  41. package/dist/xai/index.js +167 -86
  42. package/dist/xai/index.js.map +1 -1
  43. package/package.json +4 -1
  44. package/dist/chunk-MSR5P65T.js +0 -39
  45. package/dist/chunk-MSR5P65T.js.map +0 -1
  46. package/dist/chunk-SKY2JLA7.js.map +0 -1
  47. package/dist/chunk-U3FZWV4U.js.map +0 -1
  48. package/dist/chunk-U4JJC2YX.js.map +0 -1
  49. package/dist/chunk-UMKWXGO3.js.map +0 -1
  50. package/dist/chunk-Z7RBRCRN.js.map +0 -1
  51. package/dist/content-DEl3z_W2.d.ts +0 -276
  52. package/dist/image-Dhq-Yuq4.d.ts +0 -456
  53. package/dist/provider-BBMBZuGn.d.ts +0 -570
  54. /package/dist/{chunk-P5IRTEM5.js.map → chunk-7WYBJPJJ.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/id.ts","../src/types/messages.ts","../src/core/provider-handlers.ts","../src/core/provider.ts"],"sourcesContent":["/**\n * @fileoverview ID generation utilities for the Universal Provider Protocol.\n *\n * Provides functions for generating unique identifiers used throughout UPP,\n * including message IDs, tool call IDs, and other internal references.\n *\n * @module utils/id\n */\n\n/**\n * Generates a unique UUID v4 identifier.\n *\n * Uses the native `crypto.randomUUID()` when available for cryptographically\n * secure randomness. Falls back to a Math.random-based implementation for\n * environments without Web Crypto API support.\n *\n * @returns A UUID v4 string in the format `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`\n *\n * @example\n * ```typescript\n * const messageId = generateId();\n * // => \"f47ac10b-58cc-4372-a567-0e02b2c3d479\"\n * ```\n */\nexport function generateId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Generates a short alphanumeric identifier.\n *\n * Creates a 12-character random string using alphanumeric characters (a-z, A-Z, 0-9).\n * Useful for tool call IDs and other cases where a full UUID is not required.\n *\n * @param prefix - Optional prefix to prepend to the generated ID\n * @returns A string containing the prefix followed by 12 random alphanumeric characters\n *\n * @example\n * ```typescript\n * const toolCallId = generateShortId('call_');\n * // => \"call_aB3xY9mK2pQr\"\n *\n * const simpleId = generateShortId();\n * // => \"Tz4wN8vL1sHj\"\n * ```\n */\nexport function generateShortId(prefix = ''): string {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n let result = prefix;\n for (let i = 0; i < 12; i++) {\n result += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return result;\n}\n","/**\n * @fileoverview Message types for conversation history.\n *\n * Defines the message classes used to represent conversation turns\n * between users and assistants, including support for multimodal\n * content and tool calls.\n *\n * @module types/messages\n */\n\nimport { generateId } from '../utils/id.ts';\nimport type {\n ContentBlock,\n TextBlock,\n ImageBlock,\n AudioBlock,\n VideoBlock,\n UserContent,\n AssistantContent,\n} from './content.ts';\nimport type { ToolCall, ToolResult } from './tool.ts';\n\n/**\n * Message serialized to JSON format.\n * Picks common fields from Message, converts timestamp to string.\n */\nexport type MessageJSON = Pick<Message, 'id' | 'type' | 'metadata'> & {\n timestamp: string;\n content: ContentBlock[];\n toolCalls?: ToolCall[];\n results?: ToolResult[];\n};\n\n/**\n * Message type discriminator.\n *\n * Used to distinguish between different message types in a conversation.\n */\nexport type MessageType = 'user' | 'assistant' | 'tool_result';\n\n/**\n * Provider-namespaced metadata for messages.\n *\n * Each provider can attach its own metadata under its namespace,\n * preventing conflicts between different providers.\n *\n * @example\n * ```typescript\n * const metadata: MessageMetadata = {\n * openai: { model: 'gpt-4', finishReason: 'stop' },\n * anthropic: { model: 'claude-3', stopReason: 'end_turn' }\n * };\n * ```\n */\nexport interface MessageMetadata {\n [provider: string]: Record<string, unknown> | undefined;\n}\n\n/**\n * Options for constructing messages.\n */\nexport interface MessageOptions {\n /** Custom message ID (auto-generated if not provided) */\n id?: string;\n\n /** Provider-specific metadata */\n metadata?: MessageMetadata;\n}\n\n/**\n * Abstract base class for all message types.\n *\n * Provides common functionality for user, assistant, and tool result\n * messages, including content accessors and metadata handling.\n *\n * @example\n * ```typescript\n * // Access text content from any message\n * const text = message.text;\n *\n * // Access images\n * const images = message.images;\n * ```\n */\nexport abstract class Message {\n /** Unique message identifier */\n readonly id: string;\n\n /** Timestamp when the message was created */\n readonly timestamp: Date;\n\n /** Provider-specific metadata, namespaced by provider name */\n readonly metadata?: MessageMetadata;\n\n /** Message type discriminator (implemented by subclasses) */\n abstract readonly type: MessageType;\n\n /**\n * Returns the content blocks for this message.\n * Implemented by subclasses to provide type-specific content.\n */\n protected abstract getContent(): ContentBlock[];\n\n /**\n * Creates a new message instance.\n *\n * @param options - Optional message ID and metadata\n */\n constructor(options?: MessageOptions) {\n this.id = options?.id ?? generateId();\n this.timestamp = new Date();\n this.metadata = options?.metadata;\n }\n\n /**\n * Concatenated text content from all text blocks.\n * Blocks are joined with double newlines.\n */\n get text(): string {\n return this.getContent()\n .filter((block): block is TextBlock => block.type === 'text')\n .map((block) => block.text)\n .join('\\n\\n');\n }\n\n /**\n * All image content blocks in this message.\n */\n get images(): ImageBlock[] {\n return this.getContent().filter((block): block is ImageBlock => block.type === 'image');\n }\n\n /**\n * All audio content blocks in this message.\n */\n get audio(): AudioBlock[] {\n return this.getContent().filter((block): block is AudioBlock => block.type === 'audio');\n }\n\n /**\n * All video content blocks in this message.\n */\n get video(): VideoBlock[] {\n return this.getContent().filter((block): block is VideoBlock => block.type === 'video');\n }\n}\n\n/**\n * User input message.\n *\n * Represents a message from the user, which can contain text and/or\n * multimodal content like images, audio, or video.\n *\n * @example\n * ```typescript\n * // Simple text message\n * const msg = new UserMessage('Hello, world!');\n *\n * // Multimodal message\n * const msg = new UserMessage([\n * { type: 'text', text: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', url: '...' }, mimeType: 'image/png' }\n * ]);\n * ```\n */\nexport class UserMessage extends Message {\n /** Message type discriminator */\n readonly type = 'user' as const;\n\n /** Content blocks in this message */\n readonly content: UserContent[];\n\n /**\n * Creates a new user message.\n *\n * @param content - String (converted to TextBlock) or array of content blocks\n * @param options - Optional message ID and metadata\n */\n constructor(content: string | UserContent[], options?: MessageOptions) {\n super(options);\n if (typeof content === 'string') {\n this.content = [{ type: 'text', text: content }];\n } else {\n this.content = content;\n }\n }\n\n protected getContent(): ContentBlock[] {\n return this.content;\n }\n}\n\n/**\n * Assistant response message.\n *\n * Represents a response from the AI assistant, which may contain\n * text, media content, and/or tool call requests.\n *\n * @example\n * ```typescript\n * // Simple text response\n * const msg = new AssistantMessage('Hello! How can I help?');\n *\n * // Response with tool calls\n * const msg = new AssistantMessage(\n * 'Let me check the weather...',\n * [{ toolCallId: 'call_1', toolName: 'get_weather', arguments: { location: 'NYC' } }]\n * );\n * ```\n */\nexport class AssistantMessage extends Message {\n /** Message type discriminator */\n readonly type = 'assistant' as const;\n\n /** Content blocks in this message */\n readonly content: AssistantContent[];\n\n /** Tool calls requested by the model (if any) */\n readonly toolCalls?: ToolCall[];\n\n /**\n * Creates a new assistant message.\n *\n * @param content - String (converted to TextBlock) or array of content blocks\n * @param toolCalls - Tool calls requested by the model\n * @param options - Optional message ID and metadata\n */\n constructor(\n content: string | AssistantContent[],\n toolCalls?: ToolCall[],\n options?: MessageOptions\n ) {\n super(options);\n if (typeof content === 'string') {\n this.content = [{ type: 'text', text: content }];\n } else {\n this.content = content;\n }\n this.toolCalls = toolCalls;\n }\n\n protected getContent(): ContentBlock[] {\n return this.content;\n }\n\n /**\n * Whether this message contains tool call requests.\n */\n get hasToolCalls(): boolean {\n return this.toolCalls !== undefined && this.toolCalls.length > 0;\n }\n}\n\n/**\n * Tool execution result message.\n *\n * Contains the results of executing one or more tool calls,\n * sent back to the model for further processing.\n *\n * @example\n * ```typescript\n * const msg = new ToolResultMessage([\n * { toolCallId: 'call_1', result: { temperature: 72, conditions: 'sunny' } },\n * { toolCallId: 'call_2', result: 'File not found', isError: true }\n * ]);\n * ```\n */\nexport class ToolResultMessage extends Message {\n /** Message type discriminator */\n readonly type = 'tool_result' as const;\n\n /** Results from tool executions */\n readonly results: ToolResult[];\n\n /**\n * Creates a new tool result message.\n *\n * @param results - Array of tool execution results\n * @param options - Optional message ID and metadata\n */\n constructor(results: ToolResult[], options?: MessageOptions) {\n super(options);\n this.results = results;\n }\n\n protected getContent(): ContentBlock[] {\n return this.results.map((result) => ({\n type: 'text' as const,\n text:\n typeof result.result === 'string'\n ? result.result\n : JSON.stringify(result.result),\n }));\n }\n}\n\n/**\n * Type guard for UserMessage.\n *\n * @param msg - The message to check\n * @returns True if the message is a UserMessage\n *\n * @example\n * ```typescript\n * if (isUserMessage(msg)) {\n * console.log('User said:', msg.text);\n * }\n * ```\n */\nexport function isUserMessage(msg: Message): msg is UserMessage {\n return msg.type === 'user';\n}\n\n/**\n * Type guard for AssistantMessage.\n *\n * @param msg - The message to check\n * @returns True if the message is an AssistantMessage\n *\n * @example\n * ```typescript\n * if (isAssistantMessage(msg)) {\n * console.log('Assistant said:', msg.text);\n * if (msg.hasToolCalls) {\n * console.log('Tool calls:', msg.toolCalls);\n * }\n * }\n * ```\n */\nexport function isAssistantMessage(msg: Message): msg is AssistantMessage {\n return msg.type === 'assistant';\n}\n\n/**\n * Type guard for ToolResultMessage.\n *\n * @param msg - The message to check\n * @returns True if the message is a ToolResultMessage\n *\n * @example\n * ```typescript\n * if (isToolResultMessage(msg)) {\n * for (const result of msg.results) {\n * console.log(`Tool ${result.toolCallId}:`, result.result);\n * }\n * }\n * ```\n */\nexport function isToolResultMessage(msg: Message): msg is ToolResultMessage {\n return msg.type === 'tool_result';\n}\n","/**\n * @fileoverview Internal handler registry and resolver utilities.\n *\n * @module core/provider-handlers\n */\n\nimport type {\n ProviderIdentity,\n LLMHandler,\n EmbeddingHandler,\n ImageHandler,\n} from '../types/provider.ts';\n\n/**\n * Resolver for dynamically selecting LLM handlers based on model options.\n *\n * Used by providers that support multiple API modes (e.g., OpenAI with responses/completions).\n * The resolver eliminates shared mutable state by storing the mode on the ModelReference\n * and resolving the correct handler at request time.\n *\n * @typeParam TOptions - Provider-specific options type\n */\nexport interface LLMHandlerResolver<TOptions = unknown> {\n /** Map of mode identifiers to their corresponding LLM handlers */\n handlers: Record<string, LLMHandler>;\n /** The default mode when options don't specify one */\n defaultMode: string;\n /** Function to extract the mode from provider options */\n getMode: (options: TOptions | undefined) => string;\n}\n\n/**\n * Type guard to check if a value is an LLMHandlerResolver.\n */\nexport function isHandlerResolver<TOptions>(\n value: LLMHandler | LLMHandlerResolver<TOptions> | undefined\n): value is LLMHandlerResolver<TOptions> {\n return value !== undefined && 'handlers' in value && 'getMode' in value;\n}\n\ntype ProviderHandlers<TOptions = unknown> = {\n llm?: LLMHandler;\n embedding?: EmbeddingHandler;\n image?: ImageHandler;\n llmResolver?: LLMHandlerResolver<TOptions>;\n};\n\nconst providerHandlers = new WeakMap<object, ProviderHandlers<unknown>>();\n\n/**\n * Registers handler implementations for a provider.\n */\nexport function registerProviderHandlers<TOptions>(\n provider: ProviderIdentity,\n handlers: ProviderHandlers<TOptions>\n): void {\n providerHandlers.set(provider as object, handlers as ProviderHandlers<unknown>);\n}\n\nfunction getProviderHandlers<TOptions>(\n provider: ProviderIdentity\n): ProviderHandlers<TOptions> | undefined {\n return providerHandlers.get(provider as object) as ProviderHandlers<TOptions> | undefined;\n}\n\n/**\n * Resolves the correct LLM handler based on model reference options.\n *\n * For providers with multiple LLM handlers (e.g., OpenAI with responses/completions APIs),\n * this function determines which handler to use based on the options stored on the\n * ModelReference. This eliminates race conditions from shared mutable state.\n *\n * For providers with a single LLM handler, this simply returns that handler.\n *\n * @typeParam TOptions - Provider-specific options type\n * @param provider - The provider to resolve the handler from\n * @param options - The options from the ModelReference\n * @returns The resolved LLM handler, or undefined if LLM is not supported\n *\n * @internal\n */\nexport function resolveLLMHandler<TOptions = unknown>(\n provider: ProviderIdentity,\n options: TOptions | undefined\n): LLMHandler | undefined {\n const handlers = getProviderHandlers(provider);\n const resolver = handlers?.llmResolver;\n\n if (resolver) {\n const mode = resolver.getMode(options);\n return resolver.handlers[mode] ?? handlers?.llm;\n }\n\n return handlers?.llm;\n}\n\n/**\n * Resolves the embedding handler for a provider, if supported.\n *\n * @internal\n */\nexport function resolveEmbeddingHandler<TParams = unknown>(\n provider: ProviderIdentity\n): EmbeddingHandler<TParams> | undefined {\n const handlers = getProviderHandlers(provider);\n return handlers?.embedding as EmbeddingHandler<TParams> | undefined;\n}\n\n/**\n * Resolves the image handler for a provider, if supported.\n *\n * @internal\n */\nexport function resolveImageHandler<TParams = unknown>(\n provider: ProviderIdentity\n): ImageHandler<TParams> | undefined {\n const handlers = getProviderHandlers(provider);\n return handlers?.image as ImageHandler<TParams> | undefined;\n}\n","/**\n * @fileoverview Base provider interface and factory for the Universal Provider Protocol.\n *\n * This module provides the foundation for creating AI providers that conform to the\n * UPP specification. Providers are callable functions that create model references\n * and register internal handlers for LLM, embedding, and image modalities.\n *\n * @module core/provider\n */\n\nimport type {\n Provider,\n ModelReference,\n LLMHandler,\n EmbeddingHandler,\n ImageHandler,\n LLMProvider,\n EmbeddingProvider,\n ImageProvider,\n} from '../types/provider.ts';\nimport type { LLMHandlerResolver } from './provider-handlers.ts';\nimport { isHandlerResolver, registerProviderHandlers } from './provider-handlers.ts';\n\n\n/**\n * Configuration options for creating a new provider.\n *\n * @typeParam TOptions - Provider-specific options type\n *\n * @example\n * ```typescript\n * // Simple provider with single handler\n * const options: CreateProviderOptions = {\n * name: 'my-provider',\n * version: '1.0.0',\n * handlers: {\n * llm: createLLMHandler(),\n * embedding: createEmbeddingHandler(),\n * },\n * };\n *\n * // Provider with multiple LLM handlers (API modes)\n * const options: CreateProviderOptions<OpenAIOptions> = {\n * name: 'openai',\n * version: '1.0.0',\n * handlers: {\n * llm: {\n * handlers: { responses: handler1, completions: handler2 },\n * defaultMode: 'responses',\n * getMode: (opts) => opts?.api ?? 'responses',\n * },\n * },\n * };\n * ```\n */\nexport interface CreateProviderOptions<TOptions = unknown> {\n /** Unique identifier for the provider */\n name: string;\n /** Semantic version string for the provider implementation */\n version: string;\n /** Handlers for supported modalities (LLM, embedding, image generation) */\n handlers: {\n /** Handler for language model completions, or resolver for multi-handler providers */\n llm?: LLMHandler | LLMHandlerResolver<TOptions>;\n /** Handler for text embeddings */\n embedding?: EmbeddingHandler;\n /** Handler for image generation */\n image?: ImageHandler;\n };\n /**\n * Custom function to create model references from options.\n * Use this to map provider options to providerConfig (e.g., betas to headers).\n */\n createModelReference?: (\n modelId: string,\n options: TOptions | undefined,\n provider: Provider<TOptions>\n ) => ModelReference<TOptions>;\n}\n\n\n/**\n * Creates a provider factory function with registered modality handlers.\n *\n * The returned provider is a callable function that creates model references\n * when invoked with a model ID. It exposes `name` and `version` metadata.\n *\n * @typeParam TOptions - Provider-specific options type (defaults to unknown)\n * @param options - Provider configuration including name, version, and handlers\n * @returns A callable Provider with handlers registered internally\n *\n * @example\n * ```typescript\n * // Create a basic provider\n * const anthropic = createProvider({\n * name: 'anthropic',\n * version: '1.0.0',\n * handlers: { llm: createLLMHandler() },\n * });\n *\n * // Use the provider to create a model reference\n * const model = anthropic('claude-sonnet-4-20250514');\n *\n * // Provider with custom options type\n * interface MyOptions { apiVersion?: 'v1' | 'v2' }\n * const myProvider = createProvider<MyOptions>({\n * name: 'my-provider',\n * version: '1.0.0',\n * handlers: { llm: handler },\n * });\n *\n * // Provider with multiple LLM handlers (API modes)\n * const openai = createProvider<OpenAIOptions>({\n * name: 'openai',\n * version: '1.0.0',\n * handlers: {\n * llm: {\n * handlers: { responses: responsesHandler, completions: completionsHandler },\n * defaultMode: 'responses',\n * getMode: (opts) => opts?.api ?? 'responses',\n * },\n * },\n * });\n * ```\n */\nexport function createProvider<TOptions = unknown>(\n options: CreateProviderOptions<TOptions>\n): Provider<TOptions> {\n // Resolve the default LLM handler for capabilities/bind\n const llmInput = options.handlers.llm;\n const hasResolver = isHandlerResolver<TOptions>(llmInput);\n const defaultLLMHandler = hasResolver ? llmInput.handlers[llmInput.defaultMode] : llmInput;\n\n if (hasResolver && !defaultLLMHandler) {\n throw new Error(\n `Provider '${options.name}' LLM resolver defaultMode '${llmInput.defaultMode}' has no handler`\n );\n }\n\n // Create the factory function\n const fn = function (modelId: string, modelOptions?: TOptions): ModelReference<TOptions> {\n if (options.createModelReference) {\n return options.createModelReference(modelId, modelOptions, provider);\n }\n // Default: store options on the reference for handler resolution\n return { modelId, provider, options: modelOptions };\n };\n\n Object.defineProperties(fn, {\n name: {\n value: options.name,\n writable: false,\n configurable: true,\n },\n version: {\n value: options.version,\n writable: false,\n configurable: true,\n },\n });\n\n const provider = fn as Provider<TOptions>;\n\n // If there's a resolver, set provider on all handlers\n if (hasResolver) {\n for (const handler of Object.values(llmInput.handlers)) {\n handler._setProvider?.(provider as unknown as LLMProvider);\n }\n } else if (defaultLLMHandler?._setProvider) {\n defaultLLMHandler._setProvider(provider as unknown as LLMProvider);\n }\n\n if (options.handlers.embedding?._setProvider) {\n options.handlers.embedding._setProvider(provider as unknown as EmbeddingProvider);\n }\n if (options.handlers.image?._setProvider) {\n options.handlers.image._setProvider(provider as unknown as ImageProvider);\n }\n\n registerProviderHandlers(provider, {\n llm: defaultLLMHandler,\n embedding: options.handlers.embedding,\n image: options.handlers.image,\n ...(hasResolver ? { llmResolver: llmInput } : {}),\n });\n\n return provider;\n}\n"],"mappings":"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","names":[]}
@@ -56,4 +56,4 @@ export {
56
56
  emptyUsage,
57
57
  aggregateUsage
58
58
  };
59
- //# sourceMappingURL=chunk-SKY2JLA7.js.map
59
+ //# sourceMappingURL=chunk-MKDLXV4O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types/turn.ts"],"sourcesContent":["/**\n * @fileoverview Turn types for inference results.\n *\n * A Turn represents the complete result of one inference call, including\n * all messages produced during tool execution loops, token usage, and\n * optional structured output data.\n *\n * @module types/turn\n */\n\nimport type { Message, AssistantMessage } from './messages.ts';\nimport type { ToolExecution } from './tool.ts';\nimport type { MessageJSON } from './thread.ts';\n\n/**\n * Token usage information for an inference request.\n *\n * Tracks input and output tokens across all inference cycles,\n * with optional per-cycle breakdown and cache metrics.\n *\n * @example\n * ```typescript\n * const usage: TokenUsage = {\n * inputTokens: 150,\n * outputTokens: 50,\n * totalTokens: 200,\n * cacheReadTokens: 100,\n * cacheWriteTokens: 50,\n * cycles: [\n * { inputTokens: 100, outputTokens: 30, cacheReadTokens: 0, cacheWriteTokens: 50 },\n * { inputTokens: 50, outputTokens: 20, cacheReadTokens: 100, cacheWriteTokens: 0 }\n * ]\n * };\n * ```\n */\nexport interface TokenUsage {\n /** Total input tokens across all cycles */\n inputTokens: number;\n\n /** Total output tokens across all cycles */\n outputTokens: number;\n\n /** Sum of input and output tokens */\n totalTokens: number;\n\n /**\n * Tokens read from cache (cache hits).\n * Returns 0 for providers that don't support or report cache metrics.\n */\n cacheReadTokens: number;\n\n /**\n * Tokens written to cache (cache misses that were cached).\n * Only Anthropic reports this metric; returns 0 for other providers.\n */\n cacheWriteTokens: number;\n\n /** Per-cycle token breakdown (if multiple cycles occurred) */\n cycles?: Array<{\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n }>;\n}\n\n/**\n * A Turn represents the complete result of one inference call.\n *\n * Includes all messages produced during tool execution loops,\n * the final assistant response, token usage, and optional\n * structured output data.\n *\n * @typeParam TData - Type of the structured output data\n *\n * @example\n * ```typescript\n * const turn = await instance.generate('Hello');\n * console.log(turn.response.text);\n * console.log(`Used ${turn.usage.totalTokens} tokens in ${turn.cycles} cycles`);\n *\n * // With structured output\n * interface WeatherData { temperature: number; conditions: string; }\n * const turn = await instance.generate<WeatherData>('Get weather');\n * console.log(turn.data?.temperature);\n * ```\n */\nexport interface Turn<TData = unknown> {\n /**\n * All messages produced during this inference, in chronological order.\n * Includes UserMessage, AssistantMessage (may include toolCalls), and ToolResultMessage.\n */\n readonly messages: Message[];\n\n /** The final assistant response (last AssistantMessage in the turn) */\n readonly response: AssistantMessage;\n\n /**\n * Tool executions that occurred during this turn.\n *\n * Execution order reflects completion timing, not call order.\n * Correlate with tool calls using toolCallId.\n */\n readonly toolExecutions: ToolExecution[];\n\n /** Aggregate token usage for the entire turn */\n readonly usage: TokenUsage;\n\n /** Total number of inference cycles (1 + number of tool rounds) */\n readonly cycles: number;\n\n /**\n * Structured output data (if a structure schema was provided).\n * Type is inferred from the schema when using TypeScript.\n */\n readonly data?: TData;\n}\n\n/**\n * Turn serialized to JSON format.\n * Messages are converted to MessageJSON, response is omitted (computed from messages).\n *\n * @remarks\n * This type is derived from {@link Turn} and should stay in sync with it.\n */\nexport type TurnJSON = Omit<Turn, 'messages' | 'response'> & {\n messages: MessageJSON[];\n};\n\n/**\n * Creates a Turn from accumulated inference data.\n *\n * @typeParam TData - Type of the structured output data\n * @param messages - All messages produced during the inference\n * @param toolExecutions - Record of all tool executions\n * @param usage - Aggregate token usage\n * @param cycles - Number of inference cycles\n * @param data - Optional structured output data\n * @returns A complete Turn object\n * @throws Error if no assistant message is found in the messages\n *\n * @example\n * ```typescript\n * const turn = createTurn(\n * [userMsg, assistantMsg],\n * [],\n * { inputTokens: 100, outputTokens: 50, totalTokens: 150 },\n * 1\n * );\n * ```\n */\nexport function createTurn<TData = unknown>(\n messages: Message[],\n toolExecutions: ToolExecution[],\n usage: TokenUsage,\n cycles: number,\n data?: TData\n): Turn<TData> {\n const response = messages\n .filter((m): m is AssistantMessage => m.type === 'assistant')\n .pop();\n\n if (!response) {\n throw new Error('Turn must contain at least one assistant message');\n }\n\n return {\n messages,\n response,\n toolExecutions,\n usage,\n cycles,\n data,\n };\n}\n\n/**\n * Creates an empty TokenUsage object.\n *\n * @returns A TokenUsage with all values set to zero\n *\n * @example\n * ```typescript\n * const usage = emptyUsage();\n * // { inputTokens: 0, outputTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cycles: [] }\n * ```\n */\nexport function emptyUsage(): TokenUsage {\n return {\n inputTokens: 0,\n outputTokens: 0,\n totalTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n cycles: [],\n };\n}\n\n/**\n * Aggregates token usage from multiple inference cycles.\n *\n * @param usages - Array of TokenUsage objects to aggregate\n * @returns Combined TokenUsage with per-cycle breakdown\n *\n * @example\n * ```typescript\n * const cycle1 = { inputTokens: 100, outputTokens: 30, totalTokens: 130, cacheReadTokens: 50, cacheWriteTokens: 0 };\n * const cycle2 = { inputTokens: 150, outputTokens: 40, totalTokens: 190, cacheReadTokens: 100, cacheWriteTokens: 0 };\n * const total = aggregateUsage([cycle1, cycle2]);\n * // { inputTokens: 250, outputTokens: 70, totalTokens: 320, cacheReadTokens: 150, cacheWriteTokens: 0, cycles: [...] }\n * ```\n */\nexport function aggregateUsage(usages: TokenUsage[]): TokenUsage {\n const cycles: TokenUsage['cycles'] = [];\n let inputTokens = 0;\n let outputTokens = 0;\n let cacheReadTokens = 0;\n let cacheWriteTokens = 0;\n\n for (const usage of usages) {\n inputTokens += usage.inputTokens;\n outputTokens += usage.outputTokens;\n cacheReadTokens += usage.cacheReadTokens;\n cacheWriteTokens += usage.cacheWriteTokens;\n cycles.push({\n inputTokens: usage.inputTokens,\n outputTokens: usage.outputTokens,\n cacheReadTokens: usage.cacheReadTokens,\n cacheWriteTokens: usage.cacheWriteTokens,\n });\n }\n\n return {\n inputTokens,\n outputTokens,\n totalTokens: inputTokens + outputTokens,\n cacheReadTokens,\n cacheWriteTokens,\n cycles,\n };\n}\n"],"mappings":";AAuJO,SAAS,WACd,UACA,gBACA,OACA,QACA,MACa;AACb,QAAM,WAAW,SACd,OAAO,CAAC,MAA6B,EAAE,SAAS,WAAW,EAC3D,IAAI;AAEP,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAaO,SAAS,aAAyB;AACvC,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc;AAAA,IACd,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IAClB,QAAQ,CAAC;AAAA,EACX;AACF;AAgBO,SAAS,eAAe,QAAkC;AAC/D,QAAM,SAA+B,CAAC;AACtC,MAAI,cAAc;AAClB,MAAI,eAAe;AACnB,MAAI,kBAAkB;AACtB,MAAI,mBAAmB;AAEvB,aAAW,SAAS,QAAQ;AAC1B,mBAAe,MAAM;AACrB,oBAAgB,MAAM;AACtB,uBAAmB,MAAM;AACzB,wBAAoB,MAAM;AAC1B,WAAO,KAAK;AAAA,MACV,aAAa,MAAM;AAAA,MACnB,cAAc,MAAM;AAAA,MACpB,iBAAiB,MAAM;AAAA,MACvB,kBAAkB,MAAM;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,aAAa,cAAc;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1,12 +1,25 @@
1
1
  // src/http/sse.ts
2
+ var MAX_SSE_BUFFER_CHARS = 1024 * 1024;
2
3
  async function* parseSSEStream(body) {
3
4
  const reader = body.getReader();
4
5
  const decoder = new TextDecoder();
5
6
  let buffer = "";
7
+ const appendToBuffer = (chunk) => {
8
+ if (buffer.length + chunk.length > MAX_SSE_BUFFER_CHARS) {
9
+ throw new Error(
10
+ `SSE buffer exceeded maximum size (${MAX_SSE_BUFFER_CHARS} chars)`
11
+ );
12
+ }
13
+ buffer += chunk;
14
+ };
6
15
  try {
7
16
  while (true) {
8
17
  const { done, value } = await reader.read();
9
18
  if (done) {
19
+ const tail = decoder.decode();
20
+ if (tail) {
21
+ appendToBuffer(tail);
22
+ }
10
23
  if (buffer.trim()) {
11
24
  const event = parseSSEEvent(buffer);
12
25
  if (event !== null && event !== void 0) {
@@ -15,7 +28,10 @@ async function* parseSSEStream(body) {
15
28
  }
16
29
  break;
17
30
  }
18
- buffer += decoder.decode(value, { stream: true });
31
+ const chunk = decoder.decode(value, { stream: true });
32
+ if (chunk) {
33
+ appendToBuffer(chunk);
34
+ }
19
35
  const events = buffer.split(/\r?\n\r?\n/);
20
36
  buffer = events.pop() ?? "";
21
37
  for (const eventText of events) {
@@ -38,16 +54,22 @@ function parseSSEEvent(eventText) {
38
54
  let data = "";
39
55
  let eventType = "";
40
56
  for (const line of lines) {
41
- const trimmedLine = line.trim();
42
- if (trimmedLine.startsWith("event:")) {
43
- eventType = trimmedLine.slice(6).trim();
44
- } else if (trimmedLine.startsWith("data:")) {
45
- const lineData = trimmedLine.slice(5).trim();
46
- data += (data ? "\n" : "") + lineData;
47
- } else if (trimmedLine.startsWith(":")) {
57
+ const normalizedLine = line.endsWith("\r") ? line.slice(0, -1) : line;
58
+ if (normalizedLine.startsWith("event:")) {
59
+ let value = normalizedLine.slice(6);
60
+ if (value.startsWith(" ")) value = value.slice(1);
61
+ eventType = value;
62
+ } else if (normalizedLine.startsWith("data:")) {
63
+ let value = normalizedLine.slice(5);
64
+ if (value.startsWith(" ")) value = value.slice(1);
65
+ data += (data ? "\n" : "") + value;
66
+ } else if (normalizedLine.startsWith(":")) {
48
67
  continue;
49
- } else if (trimmedLine.startsWith("{") || trimmedLine.startsWith("[")) {
50
- data += (data ? "\n" : "") + trimmedLine;
68
+ } else {
69
+ const trimmedStart = normalizedLine.trimStart();
70
+ if (trimmedStart.startsWith("{") || trimmedStart.startsWith("[")) {
71
+ data += (data ? "\n" : "") + trimmedStart;
72
+ }
51
73
  }
52
74
  }
53
75
  if (!data) {
@@ -78,6 +100,10 @@ async function* parseSimpleTextStream(body) {
78
100
  yield text;
79
101
  }
80
102
  }
103
+ const remaining = decoder.decode();
104
+ if (remaining) {
105
+ yield remaining;
106
+ }
81
107
  } finally {
82
108
  reader.releaseLock();
83
109
  }
@@ -87,4 +113,4 @@ export {
87
113
  parseSSEStream,
88
114
  parseSimpleTextStream
89
115
  };
90
- //# sourceMappingURL=chunk-Z7RBRCRN.js.map
116
+ //# sourceMappingURL=chunk-NWS5IKNR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http/sse.ts"],"sourcesContent":["/**\n * Server-Sent Events (SSE) stream parsing utilities.\n * @module http/sse\n */\n\nconst MAX_SSE_BUFFER_CHARS = 1024 * 1024;\n\n/**\n * Parses a Server-Sent Events stream into JSON objects.\n *\n * This async generator handles the standard SSE wire format:\n * - Lines prefixed with \"data:\" contain event data\n * - Lines prefixed with \"event:\" specify event types\n * - Lines prefixed with \":\" are comments (used for keep-alive)\n * - Events are separated by double newlines\n * - Stream terminates on \"[DONE]\" message (OpenAI convention)\n *\n * Also handles non-standard formats used by some providers:\n * - Raw JSON without \"data:\" prefix (Google)\n * - Multi-line data fields\n *\n * @param body - ReadableStream from fetch response body\n * @yields Parsed JSON objects from each SSE event\n *\n * @example\n * ```typescript\n * const response = await doStreamFetch(url, init, config, 'openai', 'llm');\n *\n * for await (const event of parseSSEStream(response.body!)) {\n * // event is parsed JSON from each SSE data field\n * const chunk = event as OpenAIStreamChunk;\n * const delta = chunk.choices[0]?.delta?.content;\n * if (delta) {\n * process.stdout.write(delta);\n * }\n * }\n * ```\n */\nexport async function* parseSSEStream(\n body: ReadableStream<Uint8Array>\n): AsyncGenerator<unknown, void, unknown> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n const appendToBuffer = (chunk: string): void => {\n if (buffer.length + chunk.length > MAX_SSE_BUFFER_CHARS) {\n throw new Error(\n `SSE buffer exceeded maximum size (${MAX_SSE_BUFFER_CHARS} chars)`\n );\n }\n buffer += chunk;\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n const tail = decoder.decode();\n if (tail) {\n appendToBuffer(tail);\n }\n // Process any remaining data in buffer\n if (buffer.trim()) {\n const event = parseSSEEvent(buffer);\n if (event !== null && event !== undefined) {\n yield event;\n }\n }\n break;\n }\n\n const chunk = decoder.decode(value, { stream: true });\n if (chunk) {\n appendToBuffer(chunk);\n }\n\n // Process complete events (separated by double newlines or \\r\\n\\r\\n)\n const events = buffer.split(/\\r?\\n\\r?\\n/);\n\n // Keep the last partial event in the buffer\n buffer = events.pop() ?? '';\n\n for (const eventText of events) {\n if (!eventText.trim()) continue;\n\n const event = parseSSEEvent(eventText);\n if (event === 'DONE') {\n return;\n }\n if (event !== null && event !== undefined) {\n yield event;\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Parses a single SSE event block into a JSON object.\n *\n * Handles the following line prefixes:\n * - \"data:\" - Event data (multiple data lines are concatenated)\n * - \"event:\" - Event type (added to result as _eventType)\n * - \":\" - Comment (ignored, often used for keep-alive)\n * - Raw JSON starting with { or [ (provider-specific fallback)\n *\n * @param eventText - Raw text of a single SSE event block\n * @returns Parsed JSON object, 'DONE' for termination signal, or null for invalid/empty events\n */\nfunction parseSSEEvent(eventText: string): unknown | 'DONE' | null {\n const lines = eventText.split('\\n');\n let data = '';\n let eventType = '';\n\n for (const line of lines) {\n const normalizedLine = line.endsWith('\\r') ? line.slice(0, -1) : line;\n if (normalizedLine.startsWith('event:')) {\n let value = normalizedLine.slice(6);\n if (value.startsWith(' ')) value = value.slice(1);\n eventType = value;\n } else if (normalizedLine.startsWith('data:')) {\n let value = normalizedLine.slice(5);\n if (value.startsWith(' ')) value = value.slice(1);\n data += (data ? '\\n' : '') + value;\n } else if (normalizedLine.startsWith(':')) {\n continue;\n } else {\n const trimmedStart = normalizedLine.trimStart();\n if (trimmedStart.startsWith('{') || trimmedStart.startsWith('[')) {\n data += (data ? '\\n' : '') + trimmedStart;\n }\n }\n }\n\n if (!data) {\n return null;\n }\n\n if (data === '[DONE]') {\n return 'DONE';\n }\n\n try {\n const parsed = JSON.parse(data);\n\n if (eventType) {\n return { _eventType: eventType, ...parsed };\n }\n\n return parsed;\n } catch {\n return null;\n }\n}\n\n/**\n * Parses a simple text stream without SSE formatting.\n *\n * This is a simpler alternative to {@link parseSSEStream} for providers\n * that stream raw text deltas without SSE event wrappers. Each chunk\n * from the response body is decoded and yielded as-is.\n *\n * Use this for:\n * - Plain text streaming responses\n * - Providers with custom streaming formats\n * - Testing and debugging stream handling\n *\n * @param body - ReadableStream from fetch response body\n * @yields Decoded text strings from each stream chunk\n *\n * @example\n * ```typescript\n * const response = await doStreamFetch(url, init, config, 'custom', 'llm');\n *\n * for await (const text of parseSimpleTextStream(response.body!)) {\n * process.stdout.write(text);\n * }\n * ```\n */\nexport async function* parseSimpleTextStream(\n body: ReadableStream<Uint8Array>\n): AsyncGenerator<string, void, unknown> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) break;\n\n const text = decoder.decode(value, { stream: true });\n if (text) {\n yield text;\n }\n }\n const remaining = decoder.decode();\n if (remaining) {\n yield remaining;\n }\n } finally {\n reader.releaseLock();\n }\n}\n"],"mappings":"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","names":[]}
@@ -65,11 +65,30 @@ var UPPError = class _UPPError extends Error {
65
65
  }
66
66
  };
67
67
 
68
+ // src/utils/error.ts
69
+ function toError(value) {
70
+ if (value instanceof Error) {
71
+ return value;
72
+ }
73
+ if (typeof value === "string") {
74
+ return new Error(value);
75
+ }
76
+ if (typeof value === "object" && value !== null && "message" in value) {
77
+ const message = value.message;
78
+ if (typeof message === "string") {
79
+ return new Error(message);
80
+ }
81
+ }
82
+ return new Error(String(value));
83
+ }
84
+
68
85
  // src/http/errors.ts
69
86
  function statusToErrorCode(status) {
70
87
  switch (status) {
71
88
  case 400:
72
89
  return "INVALID_REQUEST";
90
+ case 402:
91
+ return "QUOTA_EXCEEDED";
73
92
  case 401:
74
93
  case 403:
75
94
  return "AUTHENTICATION_FAILED";
@@ -77,8 +96,14 @@ function statusToErrorCode(status) {
77
96
  return "MODEL_NOT_FOUND";
78
97
  case 408:
79
98
  return "TIMEOUT";
99
+ case 409:
100
+ return "INVALID_REQUEST";
101
+ case 422:
102
+ return "INVALID_REQUEST";
80
103
  case 413:
81
104
  return "CONTEXT_LENGTH_EXCEEDED";
105
+ case 451:
106
+ return "CONTENT_FILTERED";
82
107
  case 429:
83
108
  return "RATE_LIMITED";
84
109
  case 500:
@@ -93,6 +118,7 @@ function statusToErrorCode(status) {
93
118
  async function normalizeHttpError(response, provider, modality) {
94
119
  const code = statusToErrorCode(response.status);
95
120
  let message = `HTTP ${response.status}: ${response.statusText}`;
121
+ let bodyReadError;
96
122
  try {
97
123
  const body = await response.text();
98
124
  if (body) {
@@ -108,9 +134,10 @@ async function normalizeHttpError(response, provider, modality) {
108
134
  }
109
135
  }
110
136
  }
111
- } catch {
137
+ } catch (error) {
138
+ bodyReadError = toError(error);
112
139
  }
113
- return new UPPError(message, code, provider, modality, response.status);
140
+ return new UPPError(message, code, provider, modality, response.status, bodyReadError);
114
141
  }
115
142
  function networkError(error, provider, modality) {
116
143
  return new UPPError(
@@ -136,22 +163,27 @@ function cancelledError(provider, modality) {
136
163
 
137
164
  // src/http/fetch.ts
138
165
  var DEFAULT_TIMEOUT = 12e4;
166
+ var MAX_RETRY_AFTER_SECONDS = 3600;
167
+ function hasFork(strategy) {
168
+ return !!strategy && typeof strategy.fork === "function";
169
+ }
139
170
  async function doFetch(url, init, config, provider, modality) {
140
171
  const fetchFn = config.fetch ?? fetch;
141
172
  const timeout = config.timeout ?? DEFAULT_TIMEOUT;
142
- const strategy = config.retryStrategy;
143
- if (strategy?.beforeRequest) {
144
- const delay = await strategy.beforeRequest();
145
- if (delay > 0) {
146
- await sleep(delay);
147
- }
148
- }
149
- let lastError;
173
+ const baseStrategy = config.retryStrategy;
174
+ const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;
150
175
  let attempt = 0;
151
176
  while (true) {
152
177
  attempt++;
178
+ if (strategy?.beforeRequest) {
179
+ const delay = await strategy.beforeRequest();
180
+ if (delay > 0) {
181
+ await sleep(delay);
182
+ }
183
+ }
184
+ let response;
153
185
  try {
154
- const response = await fetchWithTimeout(
186
+ response = await fetchWithTimeout(
155
187
  fetchFn,
156
188
  url,
157
189
  init,
@@ -159,60 +191,61 @@ async function doFetch(url, init, config, provider, modality) {
159
191
  provider,
160
192
  modality
161
193
  );
162
- if (!response.ok) {
163
- const error = await normalizeHttpError(response, provider, modality);
164
- const retryAfter = response.headers.get("Retry-After");
165
- if (retryAfter && strategy) {
166
- const seconds = parseInt(retryAfter, 10);
167
- if (!isNaN(seconds) && "setRetryAfter" in strategy) {
168
- strategy.setRetryAfter(
169
- seconds
170
- );
171
- }
172
- }
173
- if (strategy) {
174
- const delay = await strategy.onRetry(error, attempt);
175
- if (delay !== null) {
176
- await sleep(delay);
177
- lastError = error;
178
- continue;
179
- }
180
- }
181
- throw error;
182
- }
183
- strategy?.reset?.();
184
- return response;
185
194
  } catch (error) {
186
195
  if (error instanceof UPPError) {
187
196
  if (strategy) {
188
197
  const delay = await strategy.onRetry(error, attempt);
189
198
  if (delay !== null) {
190
199
  await sleep(delay);
191
- lastError = error;
192
200
  continue;
193
201
  }
194
202
  }
195
203
  throw error;
196
204
  }
197
- const uppError = networkError(error, provider, modality);
205
+ const uppError = networkError(toError(error), provider, modality);
198
206
  if (strategy) {
199
207
  const delay = await strategy.onRetry(uppError, attempt);
200
208
  if (delay !== null) {
201
209
  await sleep(delay);
202
- lastError = uppError;
203
210
  continue;
204
211
  }
205
212
  }
206
213
  throw uppError;
207
214
  }
215
+ if (!response.ok) {
216
+ const error = await normalizeHttpError(response, provider, modality);
217
+ const retryAfterSeconds = parseRetryAfter(
218
+ response.headers.get("Retry-After"),
219
+ config.retryAfterMaxSeconds ?? MAX_RETRY_AFTER_SECONDS
220
+ );
221
+ if (retryAfterSeconds !== null && strategy && "setRetryAfter" in strategy) {
222
+ strategy.setRetryAfter(
223
+ retryAfterSeconds
224
+ );
225
+ }
226
+ if (strategy) {
227
+ const delay = await strategy.onRetry(error, attempt);
228
+ if (delay !== null) {
229
+ await sleep(delay);
230
+ continue;
231
+ }
232
+ }
233
+ throw error;
234
+ }
235
+ strategy?.reset?.();
236
+ return response;
208
237
  }
209
238
  }
210
239
  async function fetchWithTimeout(fetchFn, url, init, timeout, provider, modality) {
240
+ const existingSignal = init.signal;
241
+ if (existingSignal?.aborted) {
242
+ throw cancelledError(provider, modality);
243
+ }
211
244
  const controller = new AbortController();
212
245
  const timeoutId = setTimeout(() => controller.abort(), timeout);
213
- const existingSignal = init.signal;
246
+ const onAbort = () => controller.abort();
214
247
  if (existingSignal) {
215
- existingSignal.addEventListener("abort", () => controller.abort());
248
+ existingSignal.addEventListener("abort", onAbort, { once: true });
216
249
  }
217
250
  try {
218
251
  const response = await fetchFn(url, {
@@ -221,7 +254,7 @@ async function fetchWithTimeout(fetchFn, url, init, timeout, provider, modality)
221
254
  });
222
255
  return response;
223
256
  } catch (error) {
224
- if (error.name === "AbortError") {
257
+ if (toError(error).name === "AbortError") {
225
258
  if (existingSignal?.aborted) {
226
259
  throw cancelledError(provider, modality);
227
260
  }
@@ -230,6 +263,9 @@ async function fetchWithTimeout(fetchFn, url, init, timeout, provider, modality)
230
263
  throw error;
231
264
  } finally {
232
265
  clearTimeout(timeoutId);
266
+ if (existingSignal) {
267
+ existingSignal.removeEventListener("abort", onAbort);
268
+ }
233
269
  }
234
270
  }
235
271
  function sleep(ms) {
@@ -238,7 +274,8 @@ function sleep(ms) {
238
274
  async function doStreamFetch(url, init, config, provider, modality) {
239
275
  const fetchFn = config.fetch ?? fetch;
240
276
  const timeout = config.timeout ?? DEFAULT_TIMEOUT;
241
- const strategy = config.retryStrategy;
277
+ const baseStrategy = config.retryStrategy;
278
+ const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;
242
279
  if (strategy?.beforeRequest) {
243
280
  const delay = await strategy.beforeRequest();
244
281
  if (delay > 0) {
@@ -259,12 +296,32 @@ async function doStreamFetch(url, init, config, provider, modality) {
259
296
  if (error instanceof UPPError) {
260
297
  throw error;
261
298
  }
262
- throw networkError(error, provider, modality);
299
+ throw networkError(toError(error), provider, modality);
300
+ }
301
+ }
302
+ function parseRetryAfter(headerValue, maxSeconds) {
303
+ if (!headerValue) {
304
+ return null;
305
+ }
306
+ const seconds = parseInt(headerValue, 10);
307
+ if (!Number.isNaN(seconds)) {
308
+ return Math.min(maxSeconds, Math.max(0, seconds));
309
+ }
310
+ const dateMillis = Date.parse(headerValue);
311
+ if (Number.isNaN(dateMillis)) {
312
+ return null;
313
+ }
314
+ const deltaMs = dateMillis - Date.now();
315
+ if (deltaMs <= 0) {
316
+ return 0;
263
317
  }
318
+ const deltaSeconds = Math.ceil(deltaMs / 1e3);
319
+ return Math.min(maxSeconds, Math.max(0, deltaSeconds));
264
320
  }
265
321
 
266
322
  export {
267
323
  UPPError,
324
+ toError,
268
325
  statusToErrorCode,
269
326
  normalizeHttpError,
270
327
  networkError,
@@ -273,4 +330,4 @@ export {
273
330
  doFetch,
274
331
  doStreamFetch
275
332
  };
276
- //# sourceMappingURL=chunk-U3FZWV4U.js.map
333
+ //# sourceMappingURL=chunk-RFWLEFAB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types/errors.ts","../src/utils/error.ts","../src/http/errors.ts","../src/http/fetch.ts"],"sourcesContent":["/**\n * @fileoverview Error types for the Unified Provider Protocol.\n *\n * Provides normalized error codes and a unified error class for handling\n * errors across different AI providers in a consistent manner.\n *\n * @module types/errors\n */\n\n/**\n * Normalized error codes for cross-provider error handling.\n *\n * These codes provide a consistent way to identify and handle errors\n * regardless of which AI provider generated them.\n *\n * @example\n * ```typescript\n * try {\n * await llm.generate('Hello');\n * } catch (error) {\n * if (error instanceof UPPError) {\n * switch (error.code) {\n * case 'RATE_LIMITED':\n * await delay(error.retryAfter);\n * break;\n * case 'AUTHENTICATION_FAILED':\n * throw new Error('Invalid API key');\n * }\n * }\n * }\n * ```\n */\nexport type ErrorCode =\n /** API key is invalid or expired */\n | 'AUTHENTICATION_FAILED'\n /** Rate limit exceeded, retry after delay */\n | 'RATE_LIMITED'\n /** Input exceeds model's context window */\n | 'CONTEXT_LENGTH_EXCEEDED'\n /** Requested model does not exist */\n | 'MODEL_NOT_FOUND'\n /** Request parameters are malformed */\n | 'INVALID_REQUEST'\n /** Provider returned an unexpected response format */\n | 'INVALID_RESPONSE'\n /** Content was blocked by safety filters */\n | 'CONTENT_FILTERED'\n /** Account quota or credits exhausted */\n | 'QUOTA_EXCEEDED'\n /** Provider-specific error not covered by other codes */\n | 'PROVIDER_ERROR'\n /** Network connectivity issue */\n | 'NETWORK_ERROR'\n /** Request exceeded timeout limit */\n | 'TIMEOUT'\n /** Request was cancelled via AbortSignal */\n | 'CANCELLED';\n\n/**\n * Modality types supported by UPP.\n *\n * Each modality represents a different type of AI capability that\n * can be provided by a UPP-compatible provider.\n */\nexport type Modality =\n /** Large language model for text generation */\n | 'llm'\n /** Text/image embedding model */\n | 'embedding'\n /** Image generation model */\n | 'image'\n /** Audio processing/generation model */\n | 'audio'\n /** Video processing/generation model */\n | 'video';\n\n/**\n * Unified Provider Protocol Error.\n *\n * All provider-specific errors are normalized to this type, providing\n * a consistent interface for error handling across different AI providers.\n *\n * @example\n * ```typescript\n * throw new UPPError(\n * 'API key is invalid',\n * 'AUTHENTICATION_FAILED',\n * 'openai',\n * 'llm',\n * 401\n * );\n * ```\n *\n * @example\n * ```typescript\n * // Wrapping a provider error\n * try {\n * await openai.chat.completions.create({ ... });\n * } catch (err) {\n * throw new UPPError(\n * 'OpenAI request failed',\n * 'PROVIDER_ERROR',\n * 'openai',\n * 'llm',\n * err.status,\n * err\n * );\n * }\n * ```\n */\nexport class UPPError extends Error {\n /** Normalized error code for programmatic handling */\n readonly code: ErrorCode;\n\n /** Name of the provider that generated the error */\n readonly provider: string;\n\n /** The modality that was being used when the error occurred */\n readonly modality: Modality;\n\n /** HTTP status code from the provider's response, if available */\n readonly statusCode?: number;\n\n /** The original error that caused this UPPError, if wrapping another error */\n override readonly cause?: Error;\n\n /** Error class name, always 'UPPError' */\n override readonly name = 'UPPError';\n\n /**\n * Creates a new UPPError instance.\n *\n * @param message - Human-readable error description\n * @param code - Normalized error code for programmatic handling\n * @param provider - Name of the provider that generated the error\n * @param modality - The modality that was being used\n * @param statusCode - HTTP status code from the provider's response\n * @param cause - The original error being wrapped\n */\n constructor(\n message: string,\n code: ErrorCode,\n provider: string,\n modality: Modality,\n statusCode?: number,\n cause?: Error\n ) {\n super(message);\n this.code = code;\n this.provider = provider;\n this.modality = modality;\n this.statusCode = statusCode;\n this.cause = cause;\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, UPPError);\n }\n }\n\n /**\n * Creates a string representation of the error.\n *\n * @returns Formatted error string including code, message, provider, and modality\n */\n override toString(): string {\n let str = `UPPError [${this.code}]: ${this.message}`;\n str += ` (provider: ${this.provider}, modality: ${this.modality}`;\n if (this.statusCode) {\n str += `, status: ${this.statusCode}`;\n }\n str += ')';\n return str;\n }\n\n /**\n * Converts the error to a JSON-serializable object.\n *\n * @returns Plain object representation suitable for logging or transmission\n */\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n provider: this.provider,\n modality: this.modality,\n statusCode: this.statusCode,\n cause: this.cause?.message,\n };\n }\n}\n","/**\n * @fileoverview Error normalization utilities.\n *\n * @module utils/error\n */\n\n/**\n * Converts an unknown thrown value into an Error instance.\n *\n * @param value - Unknown error value\n * @returns An Error instance\n */\nexport function toError(value: unknown): Error {\n if (value instanceof Error) {\n return value;\n }\n if (typeof value === 'string') {\n return new Error(value);\n }\n if (typeof value === 'object' && value !== null && 'message' in value) {\n const message = (value as { message?: unknown }).message;\n if (typeof message === 'string') {\n return new Error(message);\n }\n }\n return new Error(String(value));\n}\n","/**\n * HTTP error handling and normalization utilities.\n * @module http/errors\n */\n\nimport { UPPError, type ErrorCode, type Modality } from '../types/errors.ts';\nimport { toError } from '../utils/error.ts';\n\n/**\n * Maps HTTP status codes to standardized UPP error codes.\n *\n * This function provides consistent error categorization across all providers:\n * - 400 -> INVALID_REQUEST (bad request format or parameters)\n * - 401, 403 -> AUTHENTICATION_FAILED (invalid or missing credentials)\n * - 404 -> MODEL_NOT_FOUND (requested model does not exist)\n * - 408 -> TIMEOUT (request timed out)\n * - 413 -> CONTEXT_LENGTH_EXCEEDED (input too long)\n * - 429 -> RATE_LIMITED (too many requests)\n * - 5xx -> PROVIDER_ERROR (server-side issues)\n *\n * @param status - HTTP status code from the response\n * @returns The corresponding UPP ErrorCode\n *\n * @example\n * ```typescript\n * const errorCode = statusToErrorCode(429);\n * // Returns 'RATE_LIMITED'\n *\n * const serverError = statusToErrorCode(503);\n * // Returns 'PROVIDER_ERROR'\n * ```\n */\nexport function statusToErrorCode(status: number): ErrorCode {\n switch (status) {\n case 400:\n return 'INVALID_REQUEST';\n case 402:\n return 'QUOTA_EXCEEDED';\n case 401:\n case 403:\n return 'AUTHENTICATION_FAILED';\n case 404:\n return 'MODEL_NOT_FOUND';\n case 408:\n return 'TIMEOUT';\n case 409:\n return 'INVALID_REQUEST';\n case 422:\n return 'INVALID_REQUEST';\n case 413:\n return 'CONTEXT_LENGTH_EXCEEDED';\n case 451:\n return 'CONTENT_FILTERED';\n case 429:\n return 'RATE_LIMITED';\n case 500:\n case 502:\n case 503:\n case 504:\n return 'PROVIDER_ERROR';\n default:\n return 'PROVIDER_ERROR';\n }\n}\n\n/**\n * Normalizes HTTP error responses into standardized UPPError objects.\n *\n * This function performs several operations:\n * 1. Maps the HTTP status code to an appropriate ErrorCode\n * 2. Attempts to extract a meaningful error message from the response body\n * 3. Handles various provider-specific error response formats\n *\n * Supported error message formats:\n * - `{ error: { message: \"...\" } }` (OpenAI, Anthropic)\n * - `{ message: \"...\" }` (simple format)\n * - `{ error: { error: { message: \"...\" } } }` (nested format)\n * - `{ detail: \"...\" }` (FastAPI style)\n * - Plain text body (if under 200 characters)\n *\n * @param response - The HTTP Response object with non-2xx status\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with normalized code and message\n *\n * @example\n * ```typescript\n * if (!response.ok) {\n * const error = await normalizeHttpError(response, 'openai', 'llm');\n * // error.code might be 'RATE_LIMITED' for 429\n * // error.message contains provider's error message\n * throw error;\n * }\n * ```\n */\nexport async function normalizeHttpError(\n response: Response,\n provider: string,\n modality: Modality\n): Promise<UPPError> {\n const code = statusToErrorCode(response.status);\n let message = `HTTP ${response.status}: ${response.statusText}`;\n let bodyReadError: Error | undefined;\n\n try {\n const body = await response.text();\n if (body) {\n try {\n const json = JSON.parse(body);\n const extractedMessage =\n json.error?.message ||\n json.message ||\n json.error?.error?.message ||\n json.detail;\n\n if (extractedMessage) {\n message = extractedMessage;\n }\n } catch {\n if (body.length < 200) {\n message = body;\n }\n }\n }\n } catch (error) {\n bodyReadError = toError(error);\n }\n\n return new UPPError(message, code, provider, modality, response.status, bodyReadError);\n}\n\n/**\n * Creates a UPPError for network failures (DNS, connection, etc.).\n *\n * Use this when the request fails before receiving any HTTP response,\n * such as DNS resolution failures, connection refused, or network unreachable.\n *\n * @param error - The underlying Error that caused the failure\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with NETWORK_ERROR code and the original error attached\n */\nexport function networkError(\n error: Error,\n provider: string,\n modality: Modality\n): UPPError {\n return new UPPError(\n `Network error: ${error.message}`,\n 'NETWORK_ERROR',\n provider,\n modality,\n undefined,\n error\n );\n}\n\n/**\n * Creates a UPPError for request timeout.\n *\n * Use this when the request exceeds the configured timeout duration\n * and is aborted by the AbortController.\n *\n * @param timeout - The timeout duration in milliseconds that was exceeded\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with TIMEOUT code\n */\nexport function timeoutError(\n timeout: number,\n provider: string,\n modality: Modality\n): UPPError {\n return new UPPError(\n `Request timed out after ${timeout}ms`,\n 'TIMEOUT',\n provider,\n modality\n );\n}\n\n/**\n * Creates a UPPError for user-initiated request cancellation.\n *\n * Use this when the request is aborted via a user-provided AbortSignal,\n * distinct from timeout-based cancellation.\n *\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with CANCELLED code\n */\nexport function cancelledError(provider: string, modality: Modality): UPPError {\n return new UPPError('Request was cancelled', 'CANCELLED', provider, modality);\n}\n","/**\n * HTTP fetch utilities with retry, timeout, and error normalization.\n * @module http/fetch\n */\n\nimport type { ProviderConfig, RetryStrategy } from '../types/provider.ts';\nimport type { Modality } from '../types/errors.ts';\nimport { UPPError } from '../types/errors.ts';\nimport {\n normalizeHttpError,\n networkError,\n timeoutError,\n cancelledError,\n} from './errors.ts';\nimport { toError } from '../utils/error.ts';\n\n/** Default request timeout in milliseconds (2 minutes). */\nconst DEFAULT_TIMEOUT = 120000;\nconst MAX_RETRY_AFTER_SECONDS = 3600;\n\ntype ForkableRetryStrategy = RetryStrategy & {\n fork: () => RetryStrategy | undefined;\n};\n\nfunction hasFork(strategy: RetryStrategy | undefined): strategy is ForkableRetryStrategy {\n return !!strategy && typeof (strategy as { fork?: unknown }).fork === 'function';\n}\n\n/**\n * Executes an HTTP fetch request with automatic retry, timeout handling, and error normalization.\n *\n * This function wraps the standard fetch API with additional capabilities:\n * - Configurable timeout with automatic request cancellation (per attempt)\n * - Retry strategy support (exponential backoff, linear, token bucket, etc.)\n * - Pre-request delay support for rate limiting strategies\n * - Automatic Retry-After header parsing and handling\n * - Error normalization to UPPError format\n *\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options (method, headers, body, etc.)\n * @param config - Provider configuration containing fetch customization, timeout, and retry strategy\n * @param provider - Provider identifier for error context (e.g., 'openai', 'anthropic')\n * @param modality - Request modality for error context (e.g., 'llm', 'embedding', 'image')\n * @returns The successful Response object\n *\n * @throws {UPPError} RATE_LIMITED - When rate limited and retries exhausted\n * @throws {UPPError} NETWORK_ERROR - When a network failure occurs\n * @throws {UPPError} TIMEOUT - When the request times out\n * @throws {UPPError} CANCELLED - When the request is aborted via signal\n * @throws {UPPError} Various codes based on HTTP status (see statusToErrorCode)\n *\n * @example\n * ```typescript\n * const response = await doFetch(\n * 'https://api.openai.com/v1/chat/completions',\n * {\n * method: 'POST',\n * headers: { 'Authorization': 'Bearer sk-...' },\n * body: JSON.stringify({ model: 'gpt-4', messages: [] })\n * },\n * { timeout: 30000, retryStrategy: new ExponentialBackoff() },\n * 'openai',\n * 'llm'\n * );\n * ```\n */\nexport async function doFetch(\n url: string,\n init: RequestInit,\n config: ProviderConfig,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const fetchFn = config.fetch ?? fetch;\n const timeout = config.timeout ?? DEFAULT_TIMEOUT;\n const baseStrategy = config.retryStrategy;\n const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;\n\n let attempt = 0;\n\n while (true) {\n attempt++;\n\n if (strategy?.beforeRequest) {\n const delay = await strategy.beforeRequest();\n if (delay > 0) {\n await sleep(delay);\n }\n }\n\n let response: Response;\n try {\n response = await fetchWithTimeout(\n fetchFn,\n url,\n init,\n timeout,\n provider,\n modality\n );\n } catch (error) {\n if (error instanceof UPPError) {\n if (strategy) {\n const delay = await strategy.onRetry(error, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n throw error;\n }\n\n const uppError = networkError(toError(error), provider, modality);\n\n if (strategy) {\n const delay = await strategy.onRetry(uppError, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n\n throw uppError;\n }\n\n if (!response.ok) {\n const error = await normalizeHttpError(response, provider, modality);\n\n const retryAfterSeconds = parseRetryAfter(\n response.headers.get('Retry-After'),\n config.retryAfterMaxSeconds ?? MAX_RETRY_AFTER_SECONDS\n );\n if (retryAfterSeconds !== null && strategy && 'setRetryAfter' in strategy) {\n (strategy as { setRetryAfter: (s: number) => void }).setRetryAfter(\n retryAfterSeconds\n );\n }\n\n if (strategy) {\n const delay = await strategy.onRetry(error, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n\n throw error;\n }\n\n strategy?.reset?.();\n\n return response;\n }\n}\n\n/**\n * Executes a fetch request with configurable timeout.\n *\n * Creates an AbortController to cancel the request if it exceeds the timeout.\n * Properly handles both user-provided abort signals and timeout-based cancellation,\n * throwing appropriate error types for each case.\n *\n * @param fetchFn - The fetch function to use (allows custom implementations)\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options\n * @param timeout - Maximum time in milliseconds before aborting\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns The Response from the fetch call\n *\n * @throws {UPPError} TIMEOUT - When the timeout is exceeded\n * @throws {UPPError} CANCELLED - When cancelled via user-provided signal\n * @throws {Error} Network errors are passed through unchanged\n */\nasync function fetchWithTimeout(\n fetchFn: typeof fetch,\n url: string,\n init: RequestInit,\n timeout: number,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const existingSignal = init.signal;\n\n // Check if already aborted before starting\n if (existingSignal?.aborted) {\n throw cancelledError(provider, modality);\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const onAbort = () => controller.abort();\n if (existingSignal) {\n existingSignal.addEventListener('abort', onAbort, { once: true });\n }\n\n try {\n const response = await fetchFn(url, {\n ...init,\n signal: controller.signal,\n });\n return response;\n } catch (error) {\n if (toError(error).name === 'AbortError') {\n if (existingSignal?.aborted) {\n throw cancelledError(provider, modality);\n }\n throw timeoutError(timeout, provider, modality);\n }\n throw error;\n } finally {\n clearTimeout(timeoutId);\n if (existingSignal) {\n existingSignal.removeEventListener('abort', onAbort);\n }\n }\n}\n\n/**\n * Delays execution for a specified duration.\n *\n * @param ms - Duration to sleep in milliseconds\n * @returns Promise that resolves after the specified delay\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Executes an HTTP fetch request for streaming responses.\n *\n * Unlike {@link doFetch}, this function returns the response immediately without\n * checking the HTTP status. This is necessary for Server-Sent Events (SSE) and\n * other streaming protocols where error information may be embedded in the stream.\n *\n * The caller is responsible for:\n * - Checking response.ok and handling HTTP errors\n * - Parsing the response stream (e.g., using parseSSEStream)\n * - Handling stream-specific error conditions\n *\n * Retries are not performed for streaming requests since partial data may have\n * already been consumed by the caller.\n *\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options\n * @param config - Provider configuration containing fetch customization and timeout\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns The Response object (may have non-2xx status)\n *\n * @throws {UPPError} NETWORK_ERROR - When a network failure occurs\n * @throws {UPPError} TIMEOUT - When the request times out\n * @throws {UPPError} CANCELLED - When the request is aborted via signal\n *\n * @example\n * ```typescript\n * const response = await doStreamFetch(\n * 'https://api.openai.com/v1/chat/completions',\n * {\n * method: 'POST',\n * headers: { 'Authorization': 'Bearer sk-...' },\n * body: JSON.stringify({ model: 'gpt-4', messages: [], stream: true })\n * },\n * { timeout: 120000 },\n * 'openai',\n * 'llm'\n * );\n *\n * if (!response.ok) {\n * throw await normalizeHttpError(response, 'openai', 'llm');\n * }\n *\n * for await (const event of parseSSEStream(response.body!)) {\n * console.log(event);\n * }\n * ```\n */\nexport async function doStreamFetch(\n url: string,\n init: RequestInit,\n config: ProviderConfig,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const fetchFn = config.fetch ?? fetch;\n const timeout = config.timeout ?? DEFAULT_TIMEOUT;\n const baseStrategy = config.retryStrategy;\n const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;\n\n if (strategy?.beforeRequest) {\n const delay = await strategy.beforeRequest();\n if (delay > 0) {\n await sleep(delay);\n }\n }\n\n try {\n const response = await fetchWithTimeout(\n fetchFn,\n url,\n init,\n timeout,\n provider,\n modality\n );\n return response;\n } catch (error) {\n if (error instanceof UPPError) {\n throw error;\n }\n throw networkError(toError(error), provider, modality);\n }\n}\n\n/**\n * Parses Retry-After header values into seconds.\n *\n * Supports both delta-seconds and HTTP-date formats.\n */\nfunction parseRetryAfter(headerValue: string | null, maxSeconds: number): number | null {\n if (!headerValue) {\n return null;\n }\n\n const seconds = parseInt(headerValue, 10);\n if (!Number.isNaN(seconds)) {\n return Math.min(maxSeconds, Math.max(0, seconds));\n }\n\n const dateMillis = Date.parse(headerValue);\n if (Number.isNaN(dateMillis)) {\n return null;\n }\n\n const deltaMs = dateMillis - Date.now();\n if (deltaMs <= 0) {\n return 0;\n }\n\n const deltaSeconds = Math.ceil(deltaMs / 1000);\n return Math.min(maxSeconds, Math.max(0, deltaSeconds));\n}\n"],"mappings":"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","names":[]}
@@ -96,7 +96,7 @@ var NoRetry = class {
96
96
  *
97
97
  * @returns Always returns null
98
98
  */
99
- onRetry() {
99
+ onRetry(_error, _attempt) {
100
100
  return null;
101
101
  }
102
102
  };
@@ -106,6 +106,7 @@ var TokenBucket = class {
106
106
  refillRate;
107
107
  lastRefill;
108
108
  maxAttempts;
109
+ lock;
109
110
  /**
110
111
  * Creates a new TokenBucket instance.
111
112
  *
@@ -120,6 +121,7 @@ var TokenBucket = class {
120
121
  this.maxAttempts = options.maxAttempts ?? 3;
121
122
  this.tokens = this.maxTokens;
122
123
  this.lastRefill = Date.now();
124
+ this.lock = Promise.resolve();
123
125
  }
124
126
  /**
125
127
  * Called before each request to consume a token or calculate wait time.
@@ -128,16 +130,23 @@ var TokenBucket = class {
128
130
  * - Returns 0 if a token is available (consumed immediately)
129
131
  * - Returns the wait time in milliseconds until the next token
130
132
  *
133
+ * This method may allow tokens to go negative to reserve future capacity
134
+ * and avoid concurrent callers oversubscribing the same refill.
135
+ *
131
136
  * @returns Delay in milliseconds before the request can proceed
132
137
  */
133
138
  beforeRequest() {
134
- this.refill();
135
- if (this.tokens >= 1) {
139
+ return this.withLock(() => {
140
+ this.refill();
141
+ if (this.tokens >= 1) {
142
+ this.tokens -= 1;
143
+ return 0;
144
+ }
145
+ const deficit = 1 - this.tokens;
146
+ const msPerToken = 1e3 / this.refillRate;
136
147
  this.tokens -= 1;
137
- return 0;
138
- }
139
- const msPerToken = 1e3 / this.refillRate;
140
- return Math.ceil(msPerToken);
148
+ return Math.ceil(deficit * msPerToken);
149
+ });
141
150
  }
142
151
  /**
143
152
  * Handles retry logic for rate-limited requests.
@@ -164,8 +173,10 @@ var TokenBucket = class {
164
173
  * Called automatically on successful requests to restore available tokens.
165
174
  */
166
175
  reset() {
167
- this.tokens = this.maxTokens;
168
- this.lastRefill = Date.now();
176
+ void this.withLock(() => {
177
+ this.tokens = this.maxTokens;
178
+ this.lastRefill = Date.now();
179
+ });
169
180
  }
170
181
  /**
171
182
  * Refills the bucket based on elapsed time since last refill.
@@ -177,8 +188,13 @@ var TokenBucket = class {
177
188
  this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
178
189
  this.lastRefill = now;
179
190
  }
191
+ async withLock(fn) {
192
+ const next = this.lock.then(fn, fn);
193
+ this.lock = next.then(() => void 0, () => void 0);
194
+ return next;
195
+ }
180
196
  };
181
- var RetryAfterStrategy = class {
197
+ var RetryAfterStrategy = class _RetryAfterStrategy {
182
198
  maxAttempts;
183
199
  fallbackDelay;
184
200
  lastRetryAfter;
@@ -193,6 +209,15 @@ var RetryAfterStrategy = class {
193
209
  this.maxAttempts = options.maxAttempts ?? 3;
194
210
  this.fallbackDelay = options.fallbackDelay ?? 5e3;
195
211
  }
212
+ /**
213
+ * Creates a request-scoped copy of this strategy.
214
+ */
215
+ fork() {
216
+ return new _RetryAfterStrategy({
217
+ maxAttempts: this.maxAttempts,
218
+ fallbackDelay: this.fallbackDelay
219
+ });
220
+ }
196
221
  /**
197
222
  * Sets the retry delay from a Retry-After header value.
198
223
  *
@@ -231,4 +256,4 @@ export {
231
256
  TokenBucket,
232
257
  RetryAfterStrategy
233
258
  };
234
- //# sourceMappingURL=chunk-U4JJC2YX.js.map
259
+ //# sourceMappingURL=chunk-RS7C25LS.js.map