@quilltap/plugin-utils 2.6.2 → 2.7.0
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.
- package/CHANGELOG.md +10 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +35 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -16
- package/dist/index.mjs.map +1 -1
- package/dist/providers/index.d.mts +11 -0
- package/dist/providers/index.d.ts +11 -0
- package/dist/providers/index.js +34 -15
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/index.mjs +34 -15
- package/dist/providers/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/tools/parsers.ts","../src/tools/converters.ts","../src/logging/plugin-logger.ts","../src/logging/index.ts","../src/providers/openai-compatible.ts","../src/version.ts","../src/providers/request-budget.ts","../src/providers/profile-parameters.ts","../src/providers/system-messages.ts","../src/system-prompts/builder.ts","../src/builtin-tools.ts","../src/host-rewrite.ts","../src/version.generated.ts"],"sourcesContent":["/**\n * @quilltap/plugin-utils\n *\n * Utility functions for Quilltap plugin development.\n *\n * This package provides runtime utilities that complement the type definitions\n * in @quilltap/plugin-types. It includes:\n *\n * - **Tool Parsing**: Parse tool calls from any LLM provider's response format\n * - **Tool Conversion**: Convert between OpenAI, Anthropic, and Google tool formats\n * - **Logger Bridge**: Logging that integrates with Quilltap's core or runs standalone\n *\n * @packageDocumentation\n * @module @quilltap/plugin-utils\n */\n\n// ============================================================================\n// Tool Utilities\n// ============================================================================\n\nexport {\n // Parsers\n parseToolCalls,\n parseOpenAIToolCalls,\n parseAnthropicToolCalls,\n parseGoogleToolCalls,\n detectToolCallFormat,\n hasToolCalls,\n\n // Converters\n convertToAnthropicFormat,\n convertToGoogleFormat,\n convertFromAnthropicFormat,\n convertFromGoogleFormat,\n convertToolTo,\n convertToolsTo,\n applyDescriptionLimit,\n // Backward-compatible aliases\n convertOpenAIToAnthropicFormat,\n convertOpenAIToGoogleFormat,\n} from './tools';\n\nexport type {\n // Tool types (re-exported from plugin-types)\n OpenAIToolDefinition,\n UniversalTool,\n AnthropicToolDefinition,\n GoogleToolDefinition,\n ToolCall,\n ToolCallRequest,\n ToolResult,\n ToolFormatOptions,\n\n // Utility types\n ToolCallFormat,\n ToolConvertTarget,\n} from './tools';\n\n// ============================================================================\n// Logging Utilities\n// ============================================================================\n\nexport {\n // Plugin logger factory\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n\n // Re-exported from plugin-types\n createConsoleLogger,\n createNoopLogger,\n\n // Internal APIs for Quilltap core\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './logging';\n\nexport type {\n // Logger types\n PluginLoggerWithChild,\n PluginLogger,\n LogContext,\n LogLevel,\n} from './logging';\n\n// ============================================================================\n// Provider Base Classes\n// ============================================================================\n\nexport {\n OpenAICompatibleProvider,\n applyProfileParameters,\n collapseLeadingSystemMessages,\n DEFAULT_REQUEST_TIMEOUT_MS,\n resolveRequestTimeoutMs,\n buildSdkRequestOptions,\n buildSdkClientOptions,\n buildRequestAbortSignal,\n} from './providers';\n\nexport type {\n OpenAICompatibleProviderConfig,\n ProfileParamNormalizer,\n} from './providers';\n\n// ============================================================================\n// System Prompt Utilities\n// ============================================================================\n\nexport {\n // Builder functions\n createSystemPromptPlugin,\n\n // Validation utilities\n validateSystemPromptPlugin,\n} from './system-prompts';\n\nexport type {\n // Builder option types\n CreateSystemPromptPluginOptions,\n} from './system-prompts';\n\n// ============================================================================\n// Built-in Tool Names\n// ============================================================================\n\nexport {\n BUILTIN_TOOL_NAMES,\n getBuiltinToolNames,\n} from './builtin-tools';\n\n// ============================================================================\n// Host URL Rewriting\n// ============================================================================\n\nexport {\n isVMEnvironment,\n resolveHostGateway,\n rewriteLocalhostUrl,\n} from './host-rewrite';\n\n// ============================================================================\n// Quilltap App Version\n// ============================================================================\n\nexport {\n // Version injection (for Quilltap core)\n __injectQuilltapVersion,\n __clearQuilltapVersion,\n\n // Version accessors (for plugins)\n getQuilltapVersion,\n getQuilltapUserAgent,\n} from './version';\n\n// ============================================================================\n// Character Plugin Data Types (re-exported from plugin-types)\n// ============================================================================\n\nexport type {\n CharacterPluginDataEntry,\n CharacterPluginDataMap,\n} from '@quilltap/plugin-types';\n\n// ============================================================================\n// Package Version\n// ============================================================================\n\n// Generated from package.json by scripts/generate-version.mjs — never edit the\n// literal by hand, or it drifts from the version actually published.\nexport { PLUGIN_UTILS_VERSION } from './version.generated';\n","/**\n * Tool Call Parsers\n *\n * Provider-specific parsers for extracting tool calls from LLM responses.\n * Each parser converts from a provider's native format to the standardized\n * ToolCallRequest format.\n *\n * @module @quilltap/plugin-utils/tools/parsers\n */\n\nimport type { ToolCallRequest } from '@quilltap/plugin-types';\n\n/**\n * Supported tool call response formats\n */\nexport type ToolCallFormat = 'openai' | 'anthropic' | 'google' | 'auto';\n\n/**\n * Parse OpenAI format tool calls from LLM response\n *\n * Extracts tool calls from OpenAI/Grok API responses which return\n * tool_calls in the message object.\n *\n * Expected response structures:\n * - `response.tool_calls` (direct)\n * - `response.choices[0].message.tool_calls` (nested)\n *\n * @param response - The raw response from provider API\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * const response = await openai.chat.completions.create({...});\n * const toolCalls = parseOpenAIToolCalls(response);\n * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]\n * ```\n */\nexport function parseOpenAIToolCalls(response: unknown): ToolCallRequest[] {\n const toolCalls: ToolCallRequest[] = [];\n\n try {\n const resp = response as Record<string, unknown>;\n\n // Handle direct tool_calls array (snake_case)\n let toolCallsArray = resp?.tool_calls as unknown[] | undefined;\n\n // Handle direct toolCalls array (camelCase - some SDKs use this)\n if (!toolCallsArray) {\n toolCallsArray = (resp as Record<string, unknown>)?.toolCalls as unknown[] | undefined;\n }\n\n // Check nested structure from non-streaming responses: choices[0].message.tool_calls\n if (!toolCallsArray) {\n const choices = resp?.choices as\n | Array<{ message?: { tool_calls?: unknown[]; toolCalls?: unknown[] } }>\n | undefined;\n toolCallsArray = choices?.[0]?.message?.tool_calls || choices?.[0]?.message?.toolCalls;\n }\n\n // Check nested structure from streaming responses: choices[0].delta.toolCalls\n // OpenRouter SDK uses camelCase and puts tool calls in delta for streaming\n if (!toolCallsArray) {\n const choices = resp?.choices as\n | Array<{ delta?: { tool_calls?: unknown[]; toolCalls?: unknown[] } }>\n | undefined;\n toolCallsArray = choices?.[0]?.delta?.tool_calls || choices?.[0]?.delta?.toolCalls;\n }\n\n if (toolCallsArray && Array.isArray(toolCallsArray) && toolCallsArray.length > 0) {\n for (const toolCall of toolCallsArray) {\n const tc = toolCall as {\n id?: string;\n type?: string;\n function?: { name: string; arguments: string };\n };\n\n if (tc.type === 'function' && tc.function) {\n const argsStr = tc.function.arguments || '{}';\n\n // During streaming, arguments may be incomplete JSON.\n // Check if the string looks like complete JSON before parsing.\n // Skip incomplete tool calls - they'll be complete in the final response.\n const trimmed = argsStr.trim();\n if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {\n // Incomplete JSON - skip this tool call for now\n continue;\n }\n\n try {\n toolCalls.push({\n name: tc.function.name,\n arguments: JSON.parse(argsStr),\n callId: tc.id || undefined,\n });\n } catch {\n // JSON parse failed (e.g., incomplete JSON during streaming)\n // This is expected during streaming - skip silently\n continue;\n }\n }\n }\n }\n } catch (error) {\n // Log error for unexpected parsing failures (not JSON parse errors)\n console.error('[plugin-utils] Error parsing OpenAI tool calls:', error);\n }\n\n return toolCalls;\n}\n\n/**\n * Parse Anthropic format tool calls from LLM response\n *\n * Extracts tool calls from Anthropic API responses which return\n * tool_use blocks in the content array.\n *\n * Expected response structure:\n * - `response.content` array with `{ type: 'tool_use', name, input }`\n *\n * @param response - The raw response from provider API\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * const response = await anthropic.messages.create({...});\n * const toolCalls = parseAnthropicToolCalls(response);\n * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]\n * ```\n */\nexport function parseAnthropicToolCalls(response: unknown): ToolCallRequest[] {\n const toolCalls: ToolCallRequest[] = [];\n\n try {\n const resp = response as Record<string, unknown>;\n\n if (!resp?.content || !Array.isArray(resp.content)) {\n return toolCalls;\n }\n\n for (const block of resp.content) {\n const b = block as { type?: string; id?: string; name?: string; input?: Record<string, unknown> };\n\n if (b.type === 'tool_use' && b.name) {\n toolCalls.push({\n name: b.name,\n arguments: b.input || {},\n callId: b.id || undefined,\n });\n }\n }\n } catch (error) {\n console.error('[plugin-utils] Error parsing Anthropic tool calls:', error);\n }\n\n return toolCalls;\n}\n\n/**\n * Parse Google Gemini format tool calls from LLM response\n *\n * Extracts tool calls from Google Gemini API responses which return\n * functionCall objects in the parts array.\n *\n * Expected response structure:\n * - `response.candidates[0].content.parts` array with `{ functionCall: { name, args } }`\n *\n * @param response - The raw response from provider API\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * const response = await gemini.generateContent({...});\n * const toolCalls = parseGoogleToolCalls(response);\n * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]\n * ```\n */\nexport function parseGoogleToolCalls(response: unknown): ToolCallRequest[] {\n const toolCalls: ToolCallRequest[] = [];\n\n try {\n const resp = response as Record<string, unknown>;\n const candidates = resp?.candidates as\n | Array<{ content?: { parts?: unknown[] } }>\n | undefined;\n const parts = candidates?.[0]?.content?.parts;\n\n if (!parts || !Array.isArray(parts)) {\n return toolCalls;\n }\n\n for (const part of parts) {\n const p = part as {\n functionCall?: { name: string; args?: Record<string, unknown> };\n };\n\n if (p.functionCall) {\n toolCalls.push({\n name: p.functionCall.name,\n arguments: p.functionCall.args || {},\n });\n }\n }\n } catch (error) {\n console.error('[plugin-utils] Error parsing Google tool calls:', error);\n }\n\n return toolCalls;\n}\n\n/**\n * Detect the format of a tool call response\n *\n * Analyzes the response structure to determine which provider format it uses.\n *\n * @param response - The raw response from a provider API\n * @returns The detected format, or null if unrecognized\n */\nexport function detectToolCallFormat(response: unknown): ToolCallFormat | null {\n if (!response || typeof response !== 'object') {\n return null;\n }\n\n const resp = response as Record<string, unknown>;\n\n // OpenAI format: has tool_calls/toolCalls directly or in choices[0].message or choices[0].delta\n if (resp.tool_calls && Array.isArray(resp.tool_calls)) {\n return 'openai';\n }\n if (resp.toolCalls && Array.isArray(resp.toolCalls)) {\n return 'openai';\n }\n\n const choices = resp.choices as Array<{\n message?: { tool_calls?: unknown[]; toolCalls?: unknown[] };\n delta?: { tool_calls?: unknown[]; toolCalls?: unknown[] };\n }> | undefined;\n if (choices?.[0]?.message?.tool_calls || choices?.[0]?.message?.toolCalls) {\n return 'openai';\n }\n // Check delta for streaming responses (OpenRouter SDK uses this)\n if (choices?.[0]?.delta?.tool_calls || choices?.[0]?.delta?.toolCalls) {\n return 'openai';\n }\n\n // Anthropic format: has content array with tool_use type\n if (resp.content && Array.isArray(resp.content)) {\n const hasToolUse = (resp.content as Array<{ type?: string }>).some(\n (block) => block.type === 'tool_use'\n );\n if (hasToolUse) {\n return 'anthropic';\n }\n }\n\n // Google format: has candidates[0].content.parts with functionCall\n const candidates = resp.candidates as\n | Array<{ content?: { parts?: Array<{ functionCall?: unknown }> } }>\n | undefined;\n if (candidates?.[0]?.content?.parts) {\n const hasFunctionCall = candidates[0].content.parts.some((part) => part.functionCall);\n if (hasFunctionCall) {\n return 'google';\n }\n }\n\n return null;\n}\n\n/**\n * Parse tool calls with auto-detection or explicit format\n *\n * A unified parser that can either auto-detect the response format\n * or use a specified format. This is useful when you're not sure\n * which provider's response you're handling.\n *\n * @param response - The raw response from a provider API\n * @param format - The format to use: 'openai', 'anthropic', 'google', or 'auto'\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * // Auto-detect format\n * const toolCalls = parseToolCalls(response, 'auto');\n *\n * // Or specify format explicitly\n * const toolCalls = parseToolCalls(response, 'openai');\n * ```\n */\nexport function parseToolCalls(\n response: unknown,\n format: ToolCallFormat = 'auto'\n): ToolCallRequest[] {\n let actualFormat: ToolCallFormat | null = format;\n\n if (format === 'auto') {\n actualFormat = detectToolCallFormat(response);\n if (!actualFormat) {\n return [];\n }\n }\n\n switch (actualFormat) {\n case 'openai':\n return parseOpenAIToolCalls(response);\n case 'anthropic':\n return parseAnthropicToolCalls(response);\n case 'google':\n return parseGoogleToolCalls(response);\n default:\n return [];\n }\n}\n\n/**\n * Check if a response contains tool calls\n *\n * Quick check to determine if a response has any tool calls\n * without fully parsing them.\n *\n * @param response - The raw response from a provider API\n * @returns True if the response contains tool calls\n */\nexport function hasToolCalls(response: unknown): boolean {\n const format = detectToolCallFormat(response);\n return format !== null;\n}\n","/**\n * Tool Format Converters\n *\n * Utilities for converting between different provider tool formats.\n * The universal format (OpenAI-style) serves as the baseline for all conversions.\n *\n * @module @quilltap/plugin-utils/tools/converters\n */\n\nimport type {\n UniversalTool,\n AnthropicToolDefinition,\n GoogleToolDefinition,\n OpenAIToolDefinition,\n} from '@quilltap/plugin-types';\n\n/**\n * Convert OpenAI/Universal format tool to Anthropic format\n *\n * Anthropic uses a tool_use format with:\n * - name: string\n * - description: string\n * - input_schema: JSON schema object\n *\n * @param tool - Universal tool or OpenAI tool definition\n * @returns Tool formatted for Anthropic's tool_use\n *\n * @example\n * ```typescript\n * const anthropicTool = convertToAnthropicFormat(universalTool);\n * // Returns: {\n * // name: 'search_web',\n * // description: 'Search the web',\n * // input_schema: {\n * // type: 'object',\n * // properties: { query: { type: 'string' } },\n * // required: ['query']\n * // }\n * // }\n * ```\n */\nexport function convertToAnthropicFormat(tool: UniversalTool | OpenAIToolDefinition): AnthropicToolDefinition {\n return {\n name: tool.function.name,\n description: tool.function.description ?? '',\n input_schema: {\n type: 'object',\n properties: tool.function.parameters?.properties ?? {},\n required: tool.function.parameters?.required ?? [],\n },\n };\n}\n\n/**\n * Convert OpenAI/Universal format tool to Google Gemini format\n *\n * Google uses a function calling format with:\n * - name: string\n * - description: string\n * - parameters: JSON schema object\n *\n * @param tool - Universal tool or OpenAI tool definition\n * @returns Tool formatted for Google's functionCall\n *\n * @example\n * ```typescript\n * const googleTool = convertToGoogleFormat(universalTool);\n * // Returns: {\n * // name: 'search_web',\n * // description: 'Search the web',\n * // parameters: {\n * // type: 'object',\n * // properties: { query: { type: 'string' } },\n * // required: ['query']\n * // }\n * // }\n * ```\n */\nexport function convertToGoogleFormat(tool: UniversalTool | OpenAIToolDefinition): GoogleToolDefinition {\n return {\n name: tool.function.name,\n description: tool.function.description ?? '',\n parameters: {\n type: 'object',\n properties: tool.function.parameters?.properties ?? {},\n required: tool.function.parameters?.required ?? [],\n },\n };\n}\n\n/**\n * Convert Anthropic format tool to Universal/OpenAI format\n *\n * @param tool - Anthropic format tool\n * @returns Tool in universal OpenAI format\n */\nexport function convertFromAnthropicFormat(tool: AnthropicToolDefinition): UniversalTool {\n return {\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description ?? '',\n parameters: {\n type: 'object',\n properties: tool.input_schema.properties,\n required: tool.input_schema.required ?? [],\n },\n },\n };\n}\n\n/**\n * Convert Google format tool to Universal/OpenAI format\n *\n * @param tool - Google format tool\n * @returns Tool in universal OpenAI format\n */\nexport function convertFromGoogleFormat(tool: GoogleToolDefinition): UniversalTool {\n return {\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object',\n properties: tool.parameters.properties,\n required: tool.parameters.required,\n },\n },\n };\n}\n\n/**\n * Apply prompt/description length limit to a tool\n *\n * Modifies a tool's description if it exceeds maxBytes, appending a warning\n * that the description was truncated. This is useful for providers with\n * strict token limits.\n *\n * @param tool - Tool object (any format) with a description property\n * @param maxBytes - Maximum bytes allowed for description (including warning)\n * @returns Modified tool with truncated description if needed\n *\n * @example\n * ```typescript\n * const limitedTool = applyDescriptionLimit(tool, 500);\n * // If description > 500 bytes, truncates and adds warning\n * ```\n */\nexport function applyDescriptionLimit<T extends { description: string }>(\n tool: T,\n maxBytes: number\n): T {\n if (!tool || !tool.description) {\n return tool;\n }\n\n const warningText = ' [Note: description truncated due to length limit]';\n const maxDescBytes = maxBytes - Buffer.byteLength(warningText);\n\n if (maxDescBytes <= 0) {\n console.warn('[plugin-utils] Length limit too small for warning text:', maxBytes);\n return tool;\n }\n\n const descBytes = Buffer.byteLength(tool.description);\n\n if (descBytes > maxBytes) {\n // Truncate description to fit within the byte limit\n let truncated = tool.description;\n while (Buffer.byteLength(truncated) > maxDescBytes && truncated.length > 0) {\n truncated = truncated.slice(0, -1);\n }\n\n return {\n ...tool,\n description: truncated + warningText,\n };\n }\n\n return tool;\n}\n\n/**\n * Target format for tool conversion\n */\nexport type ToolConvertTarget = 'openai' | 'anthropic' | 'google';\n\n/**\n * Convert a universal tool to a specific provider format\n *\n * @param tool - Universal tool or OpenAI tool definition\n * @param target - Target provider format\n * @returns Tool in the target format\n */\nexport function convertToolTo(\n tool: UniversalTool | OpenAIToolDefinition,\n target: ToolConvertTarget\n): UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition {\n switch (target) {\n case 'anthropic':\n return convertToAnthropicFormat(tool);\n case 'google':\n return convertToGoogleFormat(tool);\n case 'openai':\n default:\n return tool;\n }\n}\n\n/**\n * Convert multiple tools to a specific provider format\n *\n * @param tools - Array of universal tools or OpenAI tool definitions\n * @param target - Target provider format\n * @returns Array of tools in the target format\n */\nexport function convertToolsTo(\n tools: Array<UniversalTool | OpenAIToolDefinition>,\n target: ToolConvertTarget\n): Array<UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition> {\n return tools.map((tool) => convertToolTo(tool, target));\n}\n\n// ============================================================================\n// Backward-compatible aliases\n// ============================================================================\n\n/**\n * Alias for convertToAnthropicFormat\n * @deprecated Use convertToAnthropicFormat instead\n */\nexport const convertOpenAIToAnthropicFormat = convertToAnthropicFormat;\n\n/**\n * Alias for convertToGoogleFormat\n * @deprecated Use convertToGoogleFormat instead\n */\nexport const convertOpenAIToGoogleFormat = convertToGoogleFormat;\n","/**\n * Plugin Logger Bridge\n *\n * Provides a logger factory for plugins that automatically bridges\n * to Quilltap's core logging when running inside the host application,\n * or falls back to console logging when running standalone.\n *\n * @module @quilltap/plugin-utils/logging/plugin-logger\n */\n\nimport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n/**\n * Extended logger interface with child logger support\n */\nexport interface PluginLoggerWithChild extends PluginLogger {\n /**\n * Create a child logger with additional context\n * @param additionalContext Context to merge with parent context\n * @returns A new logger with combined context\n */\n child(additionalContext: LogContext): PluginLoggerWithChild;\n}\n\n/**\n * Type for the global Quilltap logger bridge\n * Stored on globalThis to work across different npm package copies\n */\ndeclare global {\n \n var __quilltap_logger_factory:\n | ((pluginName: string) => PluginLoggerWithChild)\n | undefined;\n}\n\n/**\n * Get the core logger factory from global namespace\n *\n * @returns The injected factory or null if not in Quilltap environment\n */\nfunction getCoreLoggerFactory(): ((pluginName: string) => PluginLoggerWithChild) | null {\n return globalThis.__quilltap_logger_factory ?? null;\n}\n\n/**\n * Inject the core logger factory from Quilltap host\n *\n * This is called by Quilltap core when loading plugins to bridge\n * plugin logging into the host's logging system. Uses globalThis\n * to ensure it works even when plugins have their own copy of\n * plugin-utils in their node_modules.\n *\n * **Internal API - not for plugin use**\n *\n * @param factory A function that creates a child logger for a plugin\n */\nexport function __injectCoreLoggerFactory(\n factory: (pluginName: string) => PluginLoggerWithChild\n): void {\n globalThis.__quilltap_logger_factory = factory;\n}\n\n/**\n * Clear the injected core logger factory\n *\n * Useful for testing or when unloading the plugin system.\n *\n * **Internal API - not for plugin use**\n */\nexport function __clearCoreLoggerFactory(): void {\n globalThis.__quilltap_logger_factory = undefined;\n}\n\n/**\n * Check if a core logger has been injected\n *\n * @returns True if running inside Quilltap with core logging available\n */\nexport function hasCoreLogger(): boolean {\n return getCoreLoggerFactory() !== null;\n}\n\n/**\n * Create a console logger with child support\n *\n * @param prefix Logger prefix\n * @param minLevel Minimum log level\n * @param baseContext Base context to include in all logs\n */\nfunction createConsoleLoggerWithChild(\n prefix: string,\n minLevel: LogLevel = 'debug',\n baseContext: LogContext = {}\n): PluginLoggerWithChild {\n const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];\n const shouldLog = (level: LogLevel): boolean =>\n levels.indexOf(level) >= levels.indexOf(minLevel);\n\n const formatContext = (context?: LogContext): string => {\n const merged = { ...baseContext, ...context };\n const entries = Object.entries(merged)\n .filter(([key]) => key !== 'context')\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ');\n return entries ? ` ${entries}` : '';\n };\n\n const logger: PluginLoggerWithChild = {\n debug: (message: string, context?: LogContext): void => {\n if (shouldLog('debug')) {\n console.debug(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n info: (message: string, context?: LogContext): void => {\n if (shouldLog('info')) {\n console.info(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n warn: (message: string, context?: LogContext): void => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n error: (message: string, context?: LogContext, error?: Error): void => {\n if (shouldLog('error')) {\n console.error(\n `[${prefix}] ${message}${formatContext(context)}`,\n error ? `\\n${error.stack || error.message}` : ''\n );\n }\n },\n\n child: (additionalContext: LogContext): PluginLoggerWithChild => {\n return createConsoleLoggerWithChild(prefix, minLevel, {\n ...baseContext,\n ...additionalContext,\n });\n },\n };\n\n return logger;\n}\n\n/**\n * Create a plugin logger that bridges to Quilltap core logging\n *\n * When running inside Quilltap:\n * - Routes all logs to the core logger\n * - Tags logs with `{ plugin: pluginName, module: 'plugin' }`\n * - Logs appear in Quilltap's combined.log and console\n *\n * When running standalone:\n * - Falls back to console logging with `[pluginName]` prefix\n * - Respects the specified minimum log level\n *\n * @param pluginName - The plugin identifier (e.g., 'qtap-plugin-openai')\n * @param minLevel - Minimum log level when running standalone (default: 'debug')\n * @returns A logger instance\n *\n * @example\n * ```typescript\n * // In your plugin's provider.ts\n * import { createPluginLogger } from '@quilltap/plugin-utils';\n *\n * const logger = createPluginLogger('qtap-plugin-my-provider');\n *\n * export class MyProvider implements LLMProvider {\n * async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n * logger.debug('Sending message', { model: params.model });\n *\n * try {\n * const response = await this.client.chat({...});\n * logger.info('Received response', { tokens: response.usage?.total_tokens });\n * return response;\n * } catch (error) {\n * logger.error('Failed to send message', { model: params.model }, error as Error);\n * throw error;\n * }\n * }\n * }\n * ```\n */\nexport function createPluginLogger(\n pluginName: string,\n minLevel: LogLevel = 'debug'\n): PluginLoggerWithChild {\n // Check for core logger factory from global namespace\n const coreFactory = getCoreLoggerFactory();\n if (coreFactory) {\n return coreFactory(pluginName);\n }\n\n // Standalone mode: use enhanced console logger\n return createConsoleLoggerWithChild(pluginName, minLevel);\n}\n\n/**\n * Get the minimum log level from environment\n *\n * Checks for LOG_LEVEL or QUILLTAP_LOG_LEVEL environment variables.\n * Useful for configuring standalone plugin logging.\n *\n * @returns The configured log level, or 'info' as default\n */\nexport function getLogLevelFromEnv(): LogLevel {\n if (typeof process !== 'undefined' && process.env) {\n const envLevel = process.env.LOG_LEVEL || process.env.QUILLTAP_LOG_LEVEL;\n if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {\n return envLevel as LogLevel;\n }\n }\n return 'info';\n}\n","/**\n * Logging Utilities\n *\n * Exports the plugin logger bridge and related utilities.\n *\n * @module @quilltap/plugin-utils/logging\n */\n\nexport {\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './plugin-logger';\n\nexport type { PluginLoggerWithChild } from './plugin-logger';\n\n// Re-export logger types from plugin-types\nexport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n// Re-export logger utilities from plugin-types for convenience\nexport { createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';\n","/**\n * OpenAI-Compatible Provider Base Class\n *\n * A reusable base class for building LLM providers that use OpenAI-compatible APIs.\n * This includes services like:\n * - Local LLM servers (LM Studio, vLLM, Text Generation Web UI, Ollama with OpenAI compat)\n * - Cloud services with OpenAI-compatible APIs (Gab AI, Together AI, Fireworks, etc.)\n *\n * External plugins can extend this class to create custom providers with minimal code:\n *\n * @example\n * ```typescript\n * import { OpenAICompatibleProvider } from '@quilltap/plugin-utils';\n *\n * export class MyCustomProvider extends OpenAICompatibleProvider {\n * constructor() {\n * super({\n * baseUrl: 'https://api.my-service.com/v1',\n * providerName: 'MyService',\n * requireApiKey: true,\n * attachmentErrorMessage: 'MyService does not support file attachments',\n * });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport OpenAI from 'openai';\nimport type {\n TextProvider,\n LLMParams,\n LLMResponse,\n StreamChunk,\n PluginLogger,\n} from '@quilltap/plugin-types';\nimport { createPluginLogger } from '../logging';\nimport { getQuilltapUserAgent } from '../version';\nimport { DEFAULT_REQUEST_TIMEOUT_MS, buildSdkRequestOptions } from './request-budget';\nimport { applyProfileParameters } from './profile-parameters';\nimport { collapseLeadingSystemMessages } from './system-messages';\n\n/** Matches the OpenAI SDK default; retries never apply to a caller-supplied budget. */\nconst DEFAULT_MAX_RETRIES = 2;\n\n/**\n * Configuration options for OpenAI-compatible providers.\n *\n * Use this interface when extending OpenAICompatibleProvider to customize\n * the provider's behavior for your specific service.\n */\nexport interface OpenAICompatibleProviderConfig {\n /**\n * Base URL for the API endpoint.\n * Should include the version path (e.g., 'https://api.example.com/v1')\n */\n baseUrl: string;\n\n /**\n * Provider name used for logging context.\n * This appears in log messages to identify which provider generated them.\n * @default 'OpenAICompatible'\n */\n providerName?: string;\n\n /**\n * Whether an API key is required for this provider.\n * If true, requests will fail with an error when no API key is provided.\n * If false, requests will use 'not-needed' as the API key (for local servers).\n * @default false\n */\n requireApiKey?: boolean;\n\n /**\n * Custom error message shown when file attachments are attempted.\n * @default 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)'\n */\n attachmentErrorMessage?: string;\n\n /**\n * Default wall-clock budget per request, in milliseconds.\n *\n * The OpenAI SDK's own default is 600 000 ms, which means a service that\n * accepts a connection and then goes quiet holds the caller for ten minutes\n * before the first retry. That is far longer than any Quilltap caller wants\n * to wait, so the base class lowers it.\n *\n * Individual requests can tighten this further via `LLMParams.requestTimeoutMs`.\n *\n * @default 300000 (5 minutes)\n */\n requestTimeoutMs?: number;\n\n /**\n * How many times the SDK may retry a failed or timed-out request.\n *\n * Applies only to requests that did *not* specify their own\n * `LLMParams.requestTimeoutMs` — a caller-supplied budget is a hard ceiling\n * and never retries.\n *\n * @default 2 (the OpenAI SDK default)\n */\n maxRetries?: number;\n}\n\n/**\n * Base provider class for OpenAI-compatible APIs.\n *\n * This class implements the full LLMProvider interface using the OpenAI SDK,\n * allowing subclasses to create custom providers with just configuration.\n *\n * Features:\n * - Streaming and non-streaming chat completions\n * - API key validation\n * - Model listing\n * - Configurable API key requirements\n * - Dynamic logging with provider name context\n *\n * @remarks\n * File attachments and image generation are not supported by default,\n * as support varies across OpenAI-compatible implementations.\n */\nexport class OpenAICompatibleProvider implements TextProvider {\n /** File attachments are not supported by default */\n readonly supportsFileAttachments = false;\n /** No MIME types are supported for attachments */\n readonly supportedMimeTypes: string[] = [];\n /** Web search is not supported */\n readonly supportsWebSearch = false;\n\n /** Base URL for the API endpoint */\n protected readonly baseUrl: string;\n /** Provider name for logging */\n protected readonly providerName: string;\n /** Whether API key is required */\n protected readonly requireApiKey: boolean;\n /** Error message for attachment failures */\n protected readonly attachmentErrorMessage: string;\n /** Default per-request wall-clock budget in milliseconds */\n protected readonly requestTimeoutMs: number;\n /** Retry count for requests that carry no caller-supplied budget */\n protected readonly maxRetries: number;\n /** Logger instance */\n protected readonly logger: PluginLogger;\n\n /**\n * Whether the endpoint tolerates more than one leading `system` message.\n *\n * True for every hosted service — and so the default, which keeps the\n * outbound bytes of OpenRouter, Z.AI and every other subclass exactly as they\n * were. A subclass that points at a local runtime applying the *model's* chat\n * template sets this false: several template families reject a system message\n * anywhere but index 0, and Quilltap's context builder deliberately emits up\n * to three of them so its cache breakpoints survive. See Bug 82 and\n * {@link collapseLeadingSystemMessages}.\n */\n protected readonly acceptsRepeatedSystemMessages: boolean = true;\n\n /**\n * Apply {@link acceptsRepeatedSystemMessages} to a mapped wire-message array.\n * Called by both body builds so the streaming and non-streaming shapes cannot\n * drift apart.\n */\n protected applySystemMessagePolicy<T extends { role: string; content?: string | null }>(\n messages: T[]\n ): T[] {\n return this.acceptsRepeatedSystemMessages ? messages : collapseLeadingSystemMessages(messages);\n }\n\n /**\n * Creates a new OpenAI-compatible provider instance.\n *\n * @param config - Configuration object or base URL string (for backward compatibility)\n */\n constructor(config: string | OpenAICompatibleProviderConfig) {\n // Support both legacy string baseUrl and new config object\n if (typeof config === 'string') {\n this.baseUrl = config;\n this.providerName = 'OpenAICompatible';\n this.requireApiKey = false;\n this.attachmentErrorMessage =\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = DEFAULT_MAX_RETRIES;\n } else {\n this.baseUrl = config.baseUrl;\n this.providerName = config.providerName ?? 'OpenAICompatible';\n this.requireApiKey = config.requireApiKey ?? false;\n this.attachmentErrorMessage =\n config.attachmentErrorMessage ??\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n }\n\n this.logger = createPluginLogger(`${this.providerName}Provider`);\n }\n\n /**\n * Collects attachment failures for messages with attachments.\n * Since attachments are not supported, all attachments are marked as failed.\n *\n * @param params - LLM parameters containing messages\n * @returns Object with empty sent array and failed attachments\n */\n protected collectAttachmentFailures(\n params: LLMParams\n ): { sent: string[]; failed: { id: string; error: string }[] } {\n const failed: { id: string; error: string }[] = [];\n for (const msg of params.messages) {\n if (msg.attachments) {\n for (const attachment of msg.attachments) {\n failed.push({\n id: attachment.id,\n error: this.attachmentErrorMessage,\n });\n }\n }\n }\n return { sent: [], failed };\n }\n\n /**\n * Validates that an API key is provided when required.\n * @throws Error if API key is required but not provided\n */\n protected validateApiKeyRequirement(apiKey: string): void {\n if (this.requireApiKey && !apiKey) {\n throw new Error(`${this.providerName} provider requires an API key`);\n }\n }\n\n /**\n * Gets the effective API key to use for requests.\n * Returns 'not-needed' for providers that don't require keys.\n */\n protected getEffectiveApiKey(apiKey: string): string {\n return this.requireApiKey ? apiKey : apiKey || 'not-needed';\n }\n\n /**\n * Creates an OpenAI client configured with the provider's base URL,\n * API key, and Quilltap User-Agent header.\n *\n * @param apiKey - API key for authentication\n * @returns Configured OpenAI client instance\n */\n protected createClient(apiKey: string): OpenAI {\n return new OpenAI({\n apiKey: this.getEffectiveApiKey(apiKey),\n baseURL: this.baseUrl,\n defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n timeout: this.requestTimeoutMs,\n maxRetries: this.maxRetries,\n });\n }\n\n /**\n * Builds the per-request options for a single call.\n *\n * When the caller supplied `params.requestTimeoutMs` it is a hard ceiling:\n * the request uses that budget and does not retry, so the total time spent is\n * what the caller asked for rather than a multiple of it. Otherwise the\n * client-level defaults apply.\n *\n * Subclasses that build their own request bodies should pass the result as\n * the second argument to `client.chat.completions.create(...)`.\n *\n * @param params - LLM parameters for the request\n * @returns Request options for the OpenAI SDK, or undefined to use defaults\n */\n protected buildRequestOptions(\n params: LLMParams\n ): { timeout: number; maxRetries: number } | undefined {\n return buildSdkRequestOptions(params);\n }\n\n /**\n * Profile-parameter keys this provider forwards from\n * `LLMParams.profileParameters` onto the request body.\n *\n * **Empty by default**, so a subclass that declares nothing sends exactly the\n * bytes it sent before this mechanism existed. Subclasses opt in by\n * overriding with their own allow-list; `model`, `messages`, `stream`,\n * `stream_options` and `tools` must never appear in one.\n */\n protected readonly profileParamAllowlist: readonly string[] = [];\n\n /**\n * Per-key hook for allow-listed profile parameters. The default forwards the\n * stored value verbatim.\n *\n * Override to reshape a value (the editor stores flat strings where some APIs\n * want objects), to gate a key on the model, or — by writing into `body` and\n * returning `undefined` — to send it under a different key entirely.\n */\n protected normalizeProfileParam(\n _key: string,\n value: unknown,\n _params: LLMParams,\n _body: Record<string, unknown>\n ): unknown | undefined {\n return value;\n }\n\n /**\n * Apply {@link profileParamAllowlist} to a request body under construction.\n * Called by both body builds so the streaming and non-streaming shapes cannot\n * drift apart.\n */\n protected applyProfileParams(body: Record<string, unknown>, params: LLMParams): void {\n applyProfileParameters(\n body,\n params,\n this.profileParamAllowlist,\n (key, value, callParams, target) =>\n this.normalizeProfileParam(key, value, callParams, target)\n );\n }\n\n /**\n * Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`\n * shape, tolerating servers that hand back already-parsed argument objects\n * where the OpenAI wire format specifies a JSON string.\n */\n protected normalizeToolCalls(\n toolCalls: readonly unknown[] | undefined | null\n ): { id: string; type: 'function'; function: { name: string; arguments: string } }[] {\n if (!toolCalls || toolCalls.length === 0) return [];\n const out: { id: string; type: 'function'; function: { name: string; arguments: string } }[] = [];\n for (const raw of toolCalls) {\n const tc = raw as {\n id?: string;\n function?: { name?: string; arguments?: unknown };\n };\n if (!tc?.function?.name) continue;\n const args = tc.function.arguments;\n out.push({\n id: tc.id ?? '',\n type: 'function',\n function: {\n name: tc.function.name,\n arguments: typeof args === 'string' ? args : JSON.stringify(args ?? {}),\n },\n });\n }\n return out;\n }\n\n /**\n * Build the Chat Completions request body — the ONE build both\n * {@link sendMessage} and {@link streamMessage} call, so the streaming and\n * non-streaming shapes cannot drift apart. Maps Quilltap messages to the\n * OpenAI wire format (tool results and assistant tool calls included),\n * applies {@link applySystemMessagePolicy}, assembles the body literal, and\n * finishes with {@link applyProfileParams}.\n *\n * Subclasses that keep the base class's send/stream loops can override this\n * to reshape the body in one place.\n *\n * @param params - LLM parameters for the request\n * @param stream - Whether the body is for a streaming request; adds\n * `stream: true` and `stream_options: { include_usage: true }`\n */\n protected buildRequestBody(params: LLMParams, stream: boolean): Record<string, unknown> {\n // Map messages to OpenAI Chat Completions format, including tool messages\n const mappedMessages = params.messages\n .filter((m) => {\n // Skip tool messages without toolCallId (backward compat)\n if (m.role === 'tool' && !m.toolCallId) return false;\n return true;\n })\n .map((m) => {\n // Tool result messages\n if (m.role === 'tool' && m.toolCallId) {\n return {\n role: 'tool' as const,\n tool_call_id: m.toolCallId as string,\n content: m.content,\n };\n }\n // Assistant messages with tool calls\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: 'assistant' as const,\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc: any) => ({\n id: tc.id,\n type: tc.type,\n function: tc.function,\n })),\n };\n }\n // Standard messages (strip attachments)\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n // Endpoints whose chat template insists the system message be first and\n // singular get the leading run folded into one (Bug 82); everyone else is\n // handed the array untouched.\n const messages = this.applySystemMessagePolicy(mappedMessages);\n\n // Built as a Record so the allow-list can write non-standard keys the\n // OpenAI SDK's own types don't know about (`chat_template_kwargs`, `top_k`,\n // …). Widening happens once, here, rather than as `as any` at each write.\n const body: Record<string, unknown> = {\n model: params.model,\n messages,\n temperature: params.temperature ?? 0.7,\n max_tokens: params.maxTokens ?? 4096,\n top_p: params.topP ?? 1,\n stop: params.stop,\n // Spread here — not appended after the literal — so the streaming keys\n // keep their historical position between `stop` and `user` and the\n // serialized body stays byte-identical.\n ...(stream ? { stream: true, stream_options: { include_usage: true } } : {}),\n ...(params.cacheKey ? { user: params.cacheKey } : {}),\n // Tools arrive only once the runtime gate (the profile's \"Allow tool\n // use\") has passed, so no separate guard is needed here.\n ...(params.tools && params.tools.length > 0\n ? { tools: params.tools, tool_choice: params.toolChoice ?? 'auto' }\n : {}),\n };\n this.applyProfileParams(body, params);\n return body;\n }\n\n /**\n * Sends a message and returns the complete response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @returns Complete LLM response with content and usage statistics\n */\n async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n const body = this.buildRequestBody(params, false);\n\n try {\n const response = (await client.chat.completions.create(\n body as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,\n this.buildRequestOptions(params)\n )) as OpenAI.Chat.Completions.ChatCompletion;\n\n const choice = response.choices[0];\n const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);\n return {\n content: choice.message.content ?? '',\n finishReason: choice.finish_reason,\n usage: {\n promptTokens: response.usage?.prompt_tokens ?? 0,\n completionTokens: response.usage?.completion_tokens ?? 0,\n totalTokens: response.usage?.total_tokens ?? 0,\n },\n raw: response,\n attachmentResults,\n ...(toolCalls.length > 0 ? { toolCalls } : {}),\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in sendMessage`,\n { context: `${this.providerName}Provider.sendMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Sends a message and streams the response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @yields Stream chunks with content and final usage statistics\n */\n async *streamMessage(params: LLMParams, apiKey: string): AsyncGenerator<StreamChunk> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n const body = this.buildRequestBody(params, true);\n\n try {\n const stream = (await client.chat.completions.create(\n body as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming,\n this.buildRequestOptions(params)\n )) as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>;\n\n let chunkCount = 0;\n\n // Track usage - it may come in a separate final chunk\n let accumulatedUsage: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } | null = null;\n // Tool-call fragments arrive index-keyed, with `arguments` split across\n // deltas as raw string pieces; accumulate per index and assemble at the\n // end rather than trying to parse mid-stream.\n const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>();\n let finishReason: string | null = null;\n\n for await (const chunk of stream) {\n chunkCount++;\n const choice = chunk.choices[0];\n const content = choice?.delta?.content;\n const hasUsage = chunk.usage;\n\n // Track usage when we get it (may come in a separate final chunk)\n if (hasUsage) {\n accumulatedUsage = {\n prompt_tokens: chunk.usage?.prompt_tokens,\n completion_tokens: chunk.usage?.completion_tokens,\n total_tokens: chunk.usage?.total_tokens,\n };\n }\n\n if (choice?.delta?.tool_calls) {\n for (const tcDelta of choice.delta.tool_calls) {\n const idx = tcDelta.index ?? 0;\n const existing = toolCallAccumulator.get(idx) ?? { id: '', name: '', arguments: '' };\n if (tcDelta.id) existing.id = tcDelta.id;\n if (tcDelta.function?.name) existing.name = tcDelta.function.name;\n if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;\n toolCallAccumulator.set(idx, existing);\n }\n }\n\n if (choice?.finish_reason) finishReason = choice.finish_reason;\n\n // Yield content chunks\n if (content) {\n yield {\n content,\n done: false,\n };\n }\n }\n\n const toolCalls = Array.from(toolCallAccumulator.values())\n .filter((tc) => tc.name)\n .map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: { name: tc.name, arguments: tc.arguments },\n }));\n\n // After stream ends, yield final chunk with accumulated usage\n yield {\n content: '',\n done: true,\n usage: accumulatedUsage ? {\n promptTokens: accumulatedUsage.prompt_tokens ?? 0,\n completionTokens: accumulatedUsage.completion_tokens ?? 0,\n totalTokens: accumulatedUsage.total_tokens ?? 0,\n } : undefined,\n attachmentResults,\n // Only synthesized when the stream actually carried tool calls: the\n // host's tool detection reads `rawResponse`, and an empty one on every\n // ordinary turn would be noise in the logs for no gain.\n ...(toolCalls.length > 0\n ? {\n toolCalls,\n rawResponse: {\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: '', tool_calls: toolCalls },\n finish_reason: finishReason,\n },\n ],\n },\n }\n : {}),\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in streamMessage`,\n { context: `${this.providerName}Provider.streamMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Validates an API key by attempting to list models.\n *\n * @param apiKey - API key to validate\n * @returns true if the API key is valid, false otherwise\n */\n async validateApiKey(apiKey: string): Promise<boolean> {\n // For providers that require API key, return false if not provided\n if (this.requireApiKey && !apiKey) {\n return false;\n }\n\n try {\n const client = this.createClient(apiKey);\n await client.models.list();\n return true;\n } catch (error) {\n this.logger.error(\n `${this.providerName} API validation failed`,\n { context: `${this.providerName}Provider.validateApiKey`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return false;\n }\n }\n\n /**\n * Fetches available models from the API.\n *\n * @param apiKey - API key for authentication\n * @returns Sorted array of model IDs, or empty array on failure\n */\n async getAvailableModels(apiKey: string): Promise<string[]> {\n // For providers that require API key, return empty if not provided\n if (this.requireApiKey && !apiKey) {\n this.logger.error(`${this.providerName} provider requires an API key to fetch models`, {\n context: `${this.providerName}Provider.getAvailableModels`,\n });\n return [];\n }\n\n try {\n const client = this.createClient(apiKey);\n const models = await client.models.list();\n const modelList = models.data.map((m) => m.id).sort();\n return modelList;\n } catch (error) {\n this.logger.error(\n `Failed to fetch ${this.providerName} models`,\n { context: `${this.providerName}Provider.getAvailableModels`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return [];\n }\n }\n\n}\n","/**\n * Quilltap Version Management\n *\n * Provides access to the Quilltap application version for plugins.\n * Uses the same globalThis injection pattern as the logger factory,\n * allowing the host app to inject its version at startup.\n *\n * @module @quilltap/plugin-utils/version\n */\n\n/**\n * Global key used to store the injected Quilltap version.\n * Must match the key used by the host app's injection code.\n */\nconst GLOBAL_VERSION_KEY = '__quilltap_app_version';\n\n/**\n * Inject the Quilltap application version into the global namespace.\n *\n * This should be called early in plugin initialization (alongside the\n * logger factory injection), before any plugins are loaded. Plugins\n * can then retrieve the version via `getQuilltapVersion()`.\n *\n * @param version - The Quilltap application version string (e.g., '3.3.0')\n *\n * @example\n * ```typescript\n * import { __injectQuilltapVersion } from '@quilltap/plugin-utils';\n * import packageJson from './package.json';\n *\n * __injectQuilltapVersion(packageJson.version);\n * ```\n */\nexport function __injectQuilltapVersion(version: string): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = version;\n}\n\n/**\n * Clear the injected Quilltap version from the global namespace.\n *\n * Useful for testing or cleanup.\n */\nexport function __clearQuilltapVersion(): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = undefined;\n}\n\n/**\n * Get the Quilltap application version.\n *\n * Returns the version injected by the host app, or 'unknown' if\n * no version has been injected (e.g., running outside of Quilltap).\n *\n * @returns The Quilltap version string (e.g., '3.3.0') or 'unknown'\n */\nexport function getQuilltapVersion(): string {\n const version = (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY];\n return typeof version === 'string' ? version : 'unknown';\n}\n\n/**\n * Get the Quilltap User-Agent string for API requests.\n *\n * Returns a string in the format `Quilltap/{version}` suitable for\n * use as a User-Agent header or app identifier in API calls.\n *\n * @returns User-Agent string (e.g., 'Quilltap/3.3.0')\n *\n * @example\n * ```typescript\n * import { getQuilltapUserAgent } from '@quilltap/plugin-utils';\n *\n * const client = new OpenAI({\n * apiKey,\n * defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n * });\n * ```\n */\nexport function getQuilltapUserAgent(): string {\n return `Quilltap/${getQuilltapVersion()}`;\n}\n","/**\n * Per-request wall-clock budgets.\n *\n * Every provider SDK ships a generous default request timeout — the OpenAI and\n * Anthropic SDKs both use ten minutes — on the assumption that a slow answer is\n * better than no answer. That assumption is wrong for work Quilltap awaits\n * inline on a user-visible turn: a service that accepts the connection and then\n * goes silent holds the turn for the full ten minutes before the SDK's first\n * retry, with nothing logged, because request logs are only written when a call\n * finishes.\n *\n * `LLMParams.requestTimeoutMs` is the caller's answer to \"how long is this\n * worth waiting for\". These helpers are the single place that decides what that\n * number means, so an SDK-backed provider and a `fetch`-based one apply it the\n * same way.\n *\n * @packageDocumentation\n */\n\nimport type { LLMParams } from '@quilltap/plugin-types';\n\n/**\n * Fallback budget for a request whose caller expressed no preference.\n *\n * Half the SDK default: long enough for a slow non-streaming generation, short\n * enough that a silently-stalled endpoint fails in minutes rather than tens of\n * minutes.\n */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;\n\n/**\n * The budget for one request, in milliseconds.\n *\n * @param params - LLM parameters, whose `requestTimeoutMs` wins when set\n * @param defaultMs - Budget to use when the caller named none\n * @returns The effective budget in milliseconds\n */\nexport function resolveRequestTimeoutMs(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): number {\n const requested = params.requestTimeoutMs;\n return typeof requested === 'number' && requested > 0 ? requested : defaultMs;\n}\n\n/**\n * Per-request options for an OpenAI- or Anthropic-style SDK call.\n *\n * A caller-supplied budget is a *ceiling*, not a per-attempt allowance, so\n * retries are disabled for it — three attempts at the requested budget would\n * spend three times what the caller agreed to. Returns undefined when the\n * caller named no budget, leaving the client's own defaults in charge.\n *\n * @param params - LLM parameters for the request\n * @returns SDK request options, or undefined to use the client defaults\n */\nexport function buildSdkRequestOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>\n): { timeout: number; maxRetries: number } | undefined {\n const requested = params.requestTimeoutMs;\n if (typeof requested !== 'number' || requested <= 0) return undefined;\n return { timeout: requested, maxRetries: 0 };\n}\n\n/**\n * Client-construction options for a provider that builds a fresh SDK client\n * per call, where \"client default\" and \"this request\" are the same thing.\n *\n * Always defined, unlike {@link buildSdkRequestOptions} — a per-call client has\n * no pre-existing defaults worth preserving, so it takes the resolved budget\n * either way and only forgoes retries when the caller set a ceiling.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns `timeout` and `maxRetries` to spread into the SDK constructor\n */\nexport function buildSdkClientOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): { timeout: number; maxRetries: number } {\n const capped = typeof params.requestTimeoutMs === 'number' && params.requestTimeoutMs > 0;\n return {\n timeout: resolveRequestTimeoutMs(params, defaultMs),\n maxRetries: capped ? 0 : 2,\n };\n}\n\n/**\n * An abort signal that fires at the request's budget, for providers that call\n * `fetch` directly rather than through an SDK.\n *\n * Unlike the SDK options above this bounds the *whole* exchange, body included:\n * `fetch` resolves at the response headers, but the signal stays live while the\n * body streams. Pass a generous `defaultMs` on streaming paths, or compose with\n * the caller's own signal, so a long generation is not cut off mid-answer.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns An AbortSignal that aborts once the budget elapses\n */\nexport function buildRequestAbortSignal(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): AbortSignal {\n return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));\n}\n","/**\n * Profile-parameter passthrough\n *\n * A connection profile's `parameters` blob is free-form JSON: the host stores\n * whatever the profile editor (or a hand-edited profile) puts there and hands\n * it back to the provider as `LLMParams.profileParameters` at call time. What\n * reaches the wire is entirely the provider's business — and a provider that\n * never reads the bag drops every setting in silence, which is exactly the\n * defect Bug 71 records.\n *\n * This is the one mechanism every provider uses to forward those keys.\n *\n * **An exported function rather than a base-class method.** Only one plugin in\n * the repository extends `OpenAICompatibleProvider` (DeepSeek); Z.AI and\n * OpenRouter implement `TextProvider` directly, and Ollama speaks its own\n * REST shape. A `protected` method would have reached one of them, so the\n * mechanism is a free function usable by inheritance *and* composition, and\n * the base class simply calls it.\n *\n * @packageDocumentation\n */\n\nimport type { LLMParams } from '@quilltap/plugin-types';\n\n/**\n * Per-key hook applied to an allow-listed profile parameter on its way into\n * the request body.\n *\n * Return the value to send, or `undefined` to omit the key entirely.\n *\n * The `body` is passed so a normalizer can place a value somewhere other than\n * its own top-level key — the OpenAI-compatible plugin folds `reasoning_effort`\n * into `chat_template_kwargs` this way, then returns `undefined` so no flat\n * `reasoning_effort` key is sent. Normalizers that only reshape a value can\n * ignore the argument.\n */\nexport type ProfileParamNormalizer = (\n key: string,\n value: unknown,\n params: LLMParams,\n body: Record<string, unknown>\n) => unknown | undefined;\n\n/**\n * Copy allow-listed keys from `params.profileParameters` onto a request body.\n *\n * Rules, all of which predate this helper (they were hand-rolled identically in\n * the DeepSeek and Z.AI providers before it existed):\n *\n * - **Allow-list, never spread.** `model`, `messages`, `stream`,\n * `stream_options` and `tools` must be unreachable from a profile — a\n * misconfigured bag cannot be allowed to retarget the request.\n * - **`undefined` and `null` skip.** So does the empty string, which is what\n * the schema-driven profile editor stores for \"omit this and use the model\n * default\".\n * - **The allow-list is ordered.** Keys are applied in the order given, so a\n * normalizer that merges into an earlier key (`chat_template_kwargs`) sees\n * whatever the profile set for it.\n *\n * @param body - Request body under construction; mutated in place\n * @param params - The call's LLM parameters, carrying `profileParameters`\n * @param allowlist - Keys this provider is willing to forward\n * @param normalize - Optional per-key hook (see {@link ProfileParamNormalizer})\n */\nexport function applyProfileParameters(\n body: Record<string, unknown>,\n params: LLMParams,\n allowlist: readonly string[],\n normalize?: ProfileParamNormalizer\n): void {\n const profile = params.profileParameters;\n if (!profile || typeof profile !== 'object') return;\n const bag = profile as Record<string, unknown>;\n\n for (const key of allowlist) {\n const raw = bag[key];\n if (raw === undefined || raw === null) continue;\n // Empty string from the schema-driven profile editor means \"omit the\n // parameter and use the model default.\"\n if (typeof raw === 'string' && raw === '') continue;\n const value = normalize ? normalize(key, raw, params, body) : raw;\n if (value === undefined) continue;\n body[key] = value;\n }\n}\n","/**\n * Leading-system-message collapse.\n *\n * Quilltap's context builder deliberately emits the head of a turn as up to\n * three consecutive `system` messages — the cacheable persona prefix, the\n * static identity reinforcement, and the compressed-history summary — so that a\n * cache breakpoint on the first is not invalidated by churn in the others.\n * Hosted providers accept that happily.\n *\n * A local runtime does not answer for itself: it applies the *model's own* chat\n * template, and several families (Qwen most notably, plus Llama- and\n * Gemma-derived templates) `raise_exception` on any system message after index\n * 0. The whole request is then rejected before a token is generated, so the\n * opening greeting — one system message — works and every turn after it dies.\n *\n * The repair belongs here, at request-build time, rather than in the context\n * assembly: the blocks stay separate for every provider that wants them\n * separate, and the providers that cannot take them fold the run on the way\n * out. See Bug 82.\n *\n * @packageDocumentation\n */\n\n/** The shape this helper needs; wire messages carry more, and keep it. */\ninterface RoleAndContent {\n role: string;\n content?: string | null;\n}\n\n/**\n * Fold a run of consecutive leading `system` messages into a single one,\n * joining their contents with a blank line.\n *\n * Only the *leading* run is touched — a system message that appears later is\n * left where it is, because moving or merging it would change what the model is\n * told and when. Arrays with fewer than two leading system messages are\n * returned unchanged (the same array reference), so a provider that opts in\n * still sends byte-identical requests everywhere the problem does not arise.\n *\n * @param messages - Wire-format messages, already mapped for the endpoint\n * @returns The messages with their leading system run collapsed\n */\nexport function collapseLeadingSystemMessages<T extends RoleAndContent>(messages: T[]): T[] {\n let runLength = 0;\n while (runLength < messages.length && messages[runLength].role === 'system') {\n runLength++;\n }\n\n if (runLength < 2) return messages;\n\n const run = messages.slice(0, runLength);\n const merged: T = {\n ...run[0],\n content: run\n .map((m) => m.content ?? '')\n .filter((c) => c.length > 0)\n .join('\\n\\n'),\n };\n\n return [merged, ...messages.slice(runLength)];\n}\n","/**\n * System Prompt Plugin Builder utilities\n *\n * Provides helper functions for creating and validating system prompt plugins.\n *\n * @module @quilltap/plugin-utils/system-prompts\n */\n\nimport type {\n SystemPromptData,\n SystemPromptMetadata,\n SystemPromptPlugin,\n} from '@quilltap/plugin-types';\n\n// ============================================================================\n// BUILDER OPTIONS\n// ============================================================================\n\n/**\n * Options for creating a system prompt plugin\n */\nexport interface CreateSystemPromptPluginOptions {\n /** Plugin metadata */\n metadata: SystemPromptMetadata;\n\n /**\n * One or more system prompts.\n * Each prompt must have a unique name within the plugin.\n */\n prompts: SystemPromptData[];\n\n /**\n * Optional initialization function.\n * Called when the plugin is loaded.\n */\n initialize?: () => void | Promise<void>;\n}\n\n// ============================================================================\n// BUILDER FUNCTIONS\n// ============================================================================\n\n/**\n * Creates a system prompt plugin.\n *\n * @param options - Plugin configuration options\n * @returns A valid SystemPromptPlugin instance\n *\n * @example\n * ```typescript\n * import { createSystemPromptPlugin } from '@quilltap/plugin-utils';\n *\n * export const plugin = createSystemPromptPlugin({\n * metadata: {\n * pluginId: 'my-prompts',\n * displayName: 'My Prompt Collection',\n * description: 'Custom system prompts for various models',\n * },\n * prompts: [\n * {\n * name: 'CLAUDE_CREATIVE',\n * content: '# Creative Writing Prompt\\n\\nYou are {{char}}...',\n * modelHint: 'CLAUDE',\n * category: 'CREATIVE',\n * },\n * ],\n * });\n * ```\n */\nexport function createSystemPromptPlugin(\n options: CreateSystemPromptPluginOptions\n): SystemPromptPlugin {\n const { metadata, prompts, initialize } = options;\n\n // Validate prompts\n if (prompts.length === 0) {\n throw new Error('At least one system prompt is required');\n }\n\n // Check for duplicate names\n const names = new Set<string>();\n for (const prompt of prompts) {\n if (!prompt.name || prompt.name.trim() === '') {\n throw new Error('Prompt name is required');\n }\n if (!prompt.content || prompt.content.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires content`);\n }\n if (!prompt.modelHint || prompt.modelHint.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a modelHint`);\n }\n if (!prompt.category || prompt.category.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a category`);\n }\n if (names.has(prompt.name)) {\n throw new Error(`Duplicate prompt name: \"${prompt.name}\"`);\n }\n names.add(prompt.name);\n }\n\n // Create the plugin\n const plugin: SystemPromptPlugin = {\n metadata,\n prompts,\n };\n\n // Add initialize function if provided\n if (initialize) {\n plugin.initialize = async () => {\n await initialize();\n };\n }\n\n return plugin;\n}\n\n// ============================================================================\n// VALIDATION UTILITIES\n// ============================================================================\n\n/**\n * Validates a complete system prompt plugin\n *\n * @param plugin - The plugin to validate\n * @returns True if valid, throws Error if invalid\n */\nexport function validateSystemPromptPlugin(plugin: SystemPromptPlugin): boolean {\n // Validate metadata\n if (!plugin.metadata) {\n throw new Error('Plugin metadata is required');\n }\n\n if (!plugin.metadata.pluginId || plugin.metadata.pluginId.trim() === '') {\n throw new Error('Plugin metadata.pluginId is required');\n }\n\n if (!/^[a-z0-9-]+$/.test(plugin.metadata.pluginId)) {\n throw new Error('Plugin pluginId must be lowercase alphanumeric with hyphens only');\n }\n\n if (!plugin.metadata.displayName || plugin.metadata.displayName.trim() === '') {\n throw new Error('Plugin metadata.displayName is required');\n }\n\n // Validate prompts\n if (!plugin.prompts || !Array.isArray(plugin.prompts) || plugin.prompts.length === 0) {\n throw new Error('Plugin must have at least one prompt');\n }\n\n const names = new Set<string>();\n for (const prompt of plugin.prompts) {\n if (!prompt.name || prompt.name.trim() === '') {\n throw new Error('Prompt name is required');\n }\n if (!prompt.content || prompt.content.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires content`);\n }\n if (!prompt.modelHint || prompt.modelHint.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a modelHint`);\n }\n if (!prompt.category || prompt.category.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a category`);\n }\n if (names.has(prompt.name)) {\n throw new Error(`Duplicate prompt name: \"${prompt.name}\"`);\n }\n names.add(prompt.name);\n }\n\n return true;\n}\n","/**\n * Built-in tool names for Quilltap\n *\n * These are the tool names used by Quilltap's built-in tools.\n * Plugins that provide dynamic tools (like MCP connectors) should use\n * this list for collision detection to avoid shadowing built-in functionality.\n *\n * @module @quilltap/plugin-utils/builtin-tools\n */\n\n/**\n * Names of all built-in Quilltap tools\n *\n * This set contains the function names of tools that are built into Quilltap:\n * - `generate_image` - AI image generation\n * - `search` - Search the Scriptorium (memories, conversations, backgrounds)\n * - `search_web` - Web search (when enabled)\n * - `project_info` - Get project metadata\n * - `request_full_context` - Request full context reload (context compression)\n * - `help_search` - Search Quilltap help documentation\n * - `help_settings` - Read Quilltap instance settings\n */\nexport const BUILTIN_TOOL_NAMES = new Set([\n 'generate_image',\n 'search',\n 'search_web',\n 'project_info',\n 'request_full_context',\n 'help_search',\n 'help_settings',\n // Document editing tools (Scriptorium Phase 3.3)\n 'doc_read_file',\n 'doc_write_file',\n 'doc_str_replace',\n 'doc_insert_text',\n 'doc_grep',\n 'doc_list_files',\n 'doc_read_frontmatter',\n 'doc_update_frontmatter',\n 'doc_read_heading',\n 'doc_update_heading',\n // Document file management tools (Scriptorium Phase 3.4)\n 'doc_move_file',\n 'doc_copy_file',\n 'doc_delete_file',\n 'doc_create_folder',\n 'doc_delete_folder',\n]);\n\n/**\n * Get the set of built-in tool names\n *\n * Use this function to get the current list of reserved tool names\n * when implementing collision detection in plugins that provide\n * dynamic tools (e.g., MCP server connectors, external tool bridges).\n *\n * @returns Set of tool names that are reserved by Quilltap's built-in tools\n *\n * @example\n * ```typescript\n * import { getBuiltinToolNames } from '@quilltap/plugin-utils';\n *\n * // When generating tool names from external sources:\n * const builtinNames = getBuiltinToolNames();\n * if (builtinNames.has(proposedToolName)) {\n * // Rename or prefix the tool to avoid collision\n * proposedToolName = `external_${proposedToolName}`;\n * }\n * ```\n */\nexport function getBuiltinToolNames(): Set<string> {\n return new Set(BUILTIN_TOOL_NAMES);\n}\n","/**\n * Host URL Rewriting for Container Environments\n *\n * When Quilltap runs inside Docker — or inside a virtual machine the user has\n * built and manages themselves — `localhost` and `127.0.0.1` resolve to the\n * container/VM's own loopback, not the host machine where services like Ollama\n * or LM Studio are running.\n *\n * This module provides a single function that transparently rewrites\n * localhost URLs to point at the host, so users can configure\n * `http://localhost:11434` and have it Just Work in every environment.\n *\n * Gateway resolution order:\n * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP.\n * This is also the only supported route for a self-managed VM, where\n * Quilltap has no reliable way to detect the host gateway on its own.\n * 2. In Docker: rewrite `localhost` → `host.docker.internal`\n * (Docker Desktop DNS or --add-host on Linux handles the forwarding)\n * 3. Give up gracefully — return URL unchanged\n *\n * @module @quilltap/plugin-utils/host-rewrite\n */\n\nimport { createPluginLogger } from './logging';\nimport { existsSync, statSync } from 'node:fs';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** Hostnames that refer to the local loopback */\nconst LOCALHOST_HOSTS = new Set([\n 'localhost',\n '127.0.0.1',\n '[::1]',\n '::1',\n]);\n\n// ============================================================================\n// Cached Gateway Host\n// ============================================================================\n\nlet cachedGatewayHost: string | null | undefined; // undefined = not yet resolved\n\nconst rewriteLogger = createPluginLogger('host-rewrite');\n\n// ============================================================================\n// Environment Detection (self-contained)\n// ============================================================================\n\n/**\n * Check if running in a Docker container.\n *\n * Detects Docker by checking:\n * 1. DOCKER_CONTAINER environment variable\n * 2. Existence of /.dockerenv file\n * 3. Existence of /app directory (Quilltap Docker convention)\n */\nfunction isDockerEnvironment(): boolean {\n if (process.env.DOCKER_CONTAINER === 'true') {\n return true;\n }\n\n // Check for Docker-specific markers\n try {\n if (existsSync('/.dockerenv')) {\n return true;\n }\n // Check for /app as a directory (Quilltap Docker convention)\n const appStat = statSync('/app');\n if (appStat.isDirectory()) {\n return true;\n }\n } catch {\n // Not in Docker\n }\n\n return false;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Check if running in an environment that needs URL rewriting.\n *\n * Docker is detected automatically. A hand-rolled VM cannot be, so setting\n * `QUILLTAP_HOST_IP` is what opts one in — it both enables rewriting and\n * supplies the gateway address.\n */\nexport function isVMEnvironment(): boolean {\n return isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP;\n}\n\n/**\n * Resolve the host gateway address (IP or hostname).\n *\n * Tries multiple strategies in order; caches the result so file reads\n * only happen once per process lifetime.\n */\nexport function resolveHostGateway(): string | null {\n // Return cached result if already resolved\n if (cachedGatewayHost !== undefined) {\n return cachedGatewayHost;\n }\n\n // Strategy 1: Explicit env var override\n const envIP = process.env.QUILLTAP_HOST_IP;\n if (envIP) {\n rewriteLogger.info('Host gateway from QUILLTAP_HOST_IP', { host: envIP });\n cachedGatewayHost = envIP;\n return cachedGatewayHost;\n }\n\n // Strategy 2: Docker — use host.docker.internal directly as a hostname\n // Docker Desktop (macOS/Windows) provides built-in DNS resolution for\n // host.docker.internal via its DNS server (127.0.0.11). Linux Docker\n // needs --add-host=host.docker.internal:host-gateway (handled by the\n // start scripts). Either way, host.docker.internal correctly forwards\n // to services bound to the host's loopback (127.0.0.1).\n //\n // The bridge gateway IP (e.g. 172.17.0.1) is deliberately not used as a\n // fallback: it is only the Docker bridge interface, and services listening\n // on the host's localhost are NOT reachable through it.\n if (isDockerEnvironment()) {\n rewriteLogger.info('Docker environment detected — using host.docker.internal as gateway hostname');\n cachedGatewayHost = 'host.docker.internal';\n return cachedGatewayHost;\n }\n\n rewriteLogger.warn('Could not resolve host gateway — localhost URLs will not be rewritten');\n cachedGatewayHost = null;\n return cachedGatewayHost;\n}\n\n/**\n * Rewrite a localhost URL to point at the host gateway.\n *\n * No-ops when:\n * - Not running in a container (and QUILLTAP_HOST_IP is unset)\n * - The URL doesn't point to localhost or 127.0.0.1\n * - Gateway resolution fails\n *\n * @param url The URL to potentially rewrite\n * @returns The original URL or a rewritten version with the gateway host\n */\nexport function rewriteLocalhostUrl(url: string): string {\n // No-op on bare metal\n if (!isVMEnvironment()) {\n return url;\n }\n\n // Parse the URL to check the hostname\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Not a valid URL — return unchanged\n return url;\n }\n\n // Check if hostname is a localhost variant\n if (!LOCALHOST_HOSTS.has(parsed.hostname)) {\n return url;\n }\n\n // Resolve the gateway host\n const gatewayHost = resolveHostGateway();\n if (!gatewayHost) {\n return url;\n }\n\n // Rewrite the hostname\n parsed.hostname = gatewayHost;\n const rewritten = parsed.toString();\n\n rewriteLogger.debug('Rewrote localhost URL', {\n original: url,\n rewritten,\n gatewayHost,\n });\n\n return rewritten;\n}\n\n/**\n * Reset the cached gateway host (for testing).\n * @internal\n */\nexport function _resetGatewayCache(): void {\n cachedGatewayHost = undefined;\n}\n","/**\n * GENERATED FILE — DO NOT EDIT.\n *\n * Written by scripts/generate-version.mjs from the \"version\" field in\n * package.json. Bump the version there; this file follows automatically on\n * `npm run build` / `npm run typecheck`.\n */\n\n/**\n * Version of the plugin-utils package.\n * Can be used at runtime to check compatibility.\n */\nexport const PLUGIN_UTILS_VERSION = '2.6.2';\n"],"mappings":"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","names":["OpenAI"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/tools/parsers.ts","../src/tools/converters.ts","../src/logging/plugin-logger.ts","../src/logging/index.ts","../src/providers/openai-compatible.ts","../src/version.ts","../src/providers/request-budget.ts","../src/providers/profile-parameters.ts","../src/providers/system-messages.ts","../src/system-prompts/builder.ts","../src/builtin-tools.ts","../src/host-rewrite.ts","../src/version.generated.ts"],"sourcesContent":["/**\n * @quilltap/plugin-utils\n *\n * Utility functions for Quilltap plugin development.\n *\n * This package provides runtime utilities that complement the type definitions\n * in @quilltap/plugin-types. It includes:\n *\n * - **Tool Parsing**: Parse tool calls from any LLM provider's response format\n * - **Tool Conversion**: Convert between OpenAI, Anthropic, and Google tool formats\n * - **Logger Bridge**: Logging that integrates with Quilltap's core or runs standalone\n *\n * @packageDocumentation\n * @module @quilltap/plugin-utils\n */\n\n// ============================================================================\n// Tool Utilities\n// ============================================================================\n\nexport {\n // Parsers\n parseToolCalls,\n parseOpenAIToolCalls,\n parseAnthropicToolCalls,\n parseGoogleToolCalls,\n detectToolCallFormat,\n hasToolCalls,\n\n // Converters\n convertToAnthropicFormat,\n convertToGoogleFormat,\n convertFromAnthropicFormat,\n convertFromGoogleFormat,\n convertToolTo,\n convertToolsTo,\n applyDescriptionLimit,\n // Backward-compatible aliases\n convertOpenAIToAnthropicFormat,\n convertOpenAIToGoogleFormat,\n} from './tools';\n\nexport type {\n // Tool types (re-exported from plugin-types)\n OpenAIToolDefinition,\n UniversalTool,\n AnthropicToolDefinition,\n GoogleToolDefinition,\n ToolCall,\n ToolCallRequest,\n ToolResult,\n ToolFormatOptions,\n\n // Utility types\n ToolCallFormat,\n ToolConvertTarget,\n} from './tools';\n\n// ============================================================================\n// Logging Utilities\n// ============================================================================\n\nexport {\n // Plugin logger factory\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n\n // Re-exported from plugin-types\n createConsoleLogger,\n createNoopLogger,\n\n // Internal APIs for Quilltap core\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './logging';\n\nexport type {\n // Logger types\n PluginLoggerWithChild,\n PluginLogger,\n LogContext,\n LogLevel,\n} from './logging';\n\n// ============================================================================\n// Provider Base Classes\n// ============================================================================\n\nexport {\n OpenAICompatibleProvider,\n applyProfileParameters,\n collapseLeadingSystemMessages,\n DEFAULT_REQUEST_TIMEOUT_MS,\n resolveRequestTimeoutMs,\n buildSdkRequestOptions,\n buildSdkClientOptions,\n buildRequestAbortSignal,\n} from './providers';\n\nexport type {\n OpenAICompatibleProviderConfig,\n ProfileParamNormalizer,\n} from './providers';\n\n// ============================================================================\n// System Prompt Utilities\n// ============================================================================\n\nexport {\n // Builder functions\n createSystemPromptPlugin,\n\n // Validation utilities\n validateSystemPromptPlugin,\n} from './system-prompts';\n\nexport type {\n // Builder option types\n CreateSystemPromptPluginOptions,\n} from './system-prompts';\n\n// ============================================================================\n// Built-in Tool Names\n// ============================================================================\n\nexport {\n BUILTIN_TOOL_NAMES,\n getBuiltinToolNames,\n} from './builtin-tools';\n\n// ============================================================================\n// Host URL Rewriting\n// ============================================================================\n\nexport {\n isVMEnvironment,\n resolveHostGateway,\n rewriteLocalhostUrl,\n} from './host-rewrite';\n\n// ============================================================================\n// Quilltap App Version\n// ============================================================================\n\nexport {\n // Version injection (for Quilltap core)\n __injectQuilltapVersion,\n __clearQuilltapVersion,\n\n // Version accessors (for plugins)\n getQuilltapVersion,\n getQuilltapUserAgent,\n} from './version';\n\n// ============================================================================\n// Character Plugin Data Types (re-exported from plugin-types)\n// ============================================================================\n\nexport type {\n CharacterPluginDataEntry,\n CharacterPluginDataMap,\n} from '@quilltap/plugin-types';\n\n// ============================================================================\n// Package Version\n// ============================================================================\n\n// Generated from package.json by scripts/generate-version.mjs — never edit the\n// literal by hand, or it drifts from the version actually published.\nexport { PLUGIN_UTILS_VERSION } from './version.generated';\n","/**\n * Tool Call Parsers\n *\n * Provider-specific parsers for extracting tool calls from LLM responses.\n * Each parser converts from a provider's native format to the standardized\n * ToolCallRequest format.\n *\n * @module @quilltap/plugin-utils/tools/parsers\n */\n\nimport type { ToolCallRequest } from '@quilltap/plugin-types';\n\n/**\n * Supported tool call response formats\n */\nexport type ToolCallFormat = 'openai' | 'anthropic' | 'google' | 'auto';\n\n/**\n * Parse OpenAI format tool calls from LLM response\n *\n * Extracts tool calls from OpenAI/Grok API responses which return\n * tool_calls in the message object.\n *\n * Expected response structures:\n * - `response.tool_calls` (direct)\n * - `response.choices[0].message.tool_calls` (nested)\n *\n * @param response - The raw response from provider API\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * const response = await openai.chat.completions.create({...});\n * const toolCalls = parseOpenAIToolCalls(response);\n * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]\n * ```\n */\nexport function parseOpenAIToolCalls(response: unknown): ToolCallRequest[] {\n const toolCalls: ToolCallRequest[] = [];\n\n try {\n const resp = response as Record<string, unknown>;\n\n // Handle direct tool_calls array (snake_case)\n let toolCallsArray = resp?.tool_calls as unknown[] | undefined;\n\n // Handle direct toolCalls array (camelCase - some SDKs use this)\n if (!toolCallsArray) {\n toolCallsArray = (resp as Record<string, unknown>)?.toolCalls as unknown[] | undefined;\n }\n\n // Check nested structure from non-streaming responses: choices[0].message.tool_calls\n if (!toolCallsArray) {\n const choices = resp?.choices as\n | Array<{ message?: { tool_calls?: unknown[]; toolCalls?: unknown[] } }>\n | undefined;\n toolCallsArray = choices?.[0]?.message?.tool_calls || choices?.[0]?.message?.toolCalls;\n }\n\n // Check nested structure from streaming responses: choices[0].delta.toolCalls\n // OpenRouter SDK uses camelCase and puts tool calls in delta for streaming\n if (!toolCallsArray) {\n const choices = resp?.choices as\n | Array<{ delta?: { tool_calls?: unknown[]; toolCalls?: unknown[] } }>\n | undefined;\n toolCallsArray = choices?.[0]?.delta?.tool_calls || choices?.[0]?.delta?.toolCalls;\n }\n\n if (toolCallsArray && Array.isArray(toolCallsArray) && toolCallsArray.length > 0) {\n for (const toolCall of toolCallsArray) {\n const tc = toolCall as {\n id?: string;\n type?: string;\n function?: { name: string; arguments: string };\n };\n\n if (tc.type === 'function' && tc.function) {\n const argsStr = tc.function.arguments || '{}';\n\n // During streaming, arguments may be incomplete JSON.\n // Check if the string looks like complete JSON before parsing.\n // Skip incomplete tool calls - they'll be complete in the final response.\n const trimmed = argsStr.trim();\n if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {\n // Incomplete JSON - skip this tool call for now\n continue;\n }\n\n try {\n toolCalls.push({\n name: tc.function.name,\n arguments: JSON.parse(argsStr),\n callId: tc.id || undefined,\n });\n } catch {\n // JSON parse failed (e.g., incomplete JSON during streaming)\n // This is expected during streaming - skip silently\n continue;\n }\n }\n }\n }\n } catch (error) {\n // Log error for unexpected parsing failures (not JSON parse errors)\n console.error('[plugin-utils] Error parsing OpenAI tool calls:', error);\n }\n\n return toolCalls;\n}\n\n/**\n * Parse Anthropic format tool calls from LLM response\n *\n * Extracts tool calls from Anthropic API responses which return\n * tool_use blocks in the content array.\n *\n * Expected response structure:\n * - `response.content` array with `{ type: 'tool_use', name, input }`\n *\n * @param response - The raw response from provider API\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * const response = await anthropic.messages.create({...});\n * const toolCalls = parseAnthropicToolCalls(response);\n * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]\n * ```\n */\nexport function parseAnthropicToolCalls(response: unknown): ToolCallRequest[] {\n const toolCalls: ToolCallRequest[] = [];\n\n try {\n const resp = response as Record<string, unknown>;\n\n if (!resp?.content || !Array.isArray(resp.content)) {\n return toolCalls;\n }\n\n for (const block of resp.content) {\n const b = block as { type?: string; id?: string; name?: string; input?: Record<string, unknown> };\n\n if (b.type === 'tool_use' && b.name) {\n toolCalls.push({\n name: b.name,\n arguments: b.input || {},\n callId: b.id || undefined,\n });\n }\n }\n } catch (error) {\n console.error('[plugin-utils] Error parsing Anthropic tool calls:', error);\n }\n\n return toolCalls;\n}\n\n/**\n * Parse Google Gemini format tool calls from LLM response\n *\n * Extracts tool calls from Google Gemini API responses which return\n * functionCall objects in the parts array.\n *\n * Expected response structure:\n * - `response.candidates[0].content.parts` array with `{ functionCall: { name, args } }`\n *\n * @param response - The raw response from provider API\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * const response = await gemini.generateContent({...});\n * const toolCalls = parseGoogleToolCalls(response);\n * // Returns: [{ name: 'search_web', arguments: { query: 'hello' } }]\n * ```\n */\nexport function parseGoogleToolCalls(response: unknown): ToolCallRequest[] {\n const toolCalls: ToolCallRequest[] = [];\n\n try {\n const resp = response as Record<string, unknown>;\n const candidates = resp?.candidates as\n | Array<{ content?: { parts?: unknown[] } }>\n | undefined;\n const parts = candidates?.[0]?.content?.parts;\n\n if (!parts || !Array.isArray(parts)) {\n return toolCalls;\n }\n\n for (const part of parts) {\n const p = part as {\n functionCall?: { name: string; args?: Record<string, unknown> };\n };\n\n if (p.functionCall) {\n toolCalls.push({\n name: p.functionCall.name,\n arguments: p.functionCall.args || {},\n });\n }\n }\n } catch (error) {\n console.error('[plugin-utils] Error parsing Google tool calls:', error);\n }\n\n return toolCalls;\n}\n\n/**\n * Detect the format of a tool call response\n *\n * Analyzes the response structure to determine which provider format it uses.\n *\n * @param response - The raw response from a provider API\n * @returns The detected format, or null if unrecognized\n */\nexport function detectToolCallFormat(response: unknown): ToolCallFormat | null {\n if (!response || typeof response !== 'object') {\n return null;\n }\n\n const resp = response as Record<string, unknown>;\n\n // OpenAI format: has tool_calls/toolCalls directly or in choices[0].message or choices[0].delta\n if (resp.tool_calls && Array.isArray(resp.tool_calls)) {\n return 'openai';\n }\n if (resp.toolCalls && Array.isArray(resp.toolCalls)) {\n return 'openai';\n }\n\n const choices = resp.choices as Array<{\n message?: { tool_calls?: unknown[]; toolCalls?: unknown[] };\n delta?: { tool_calls?: unknown[]; toolCalls?: unknown[] };\n }> | undefined;\n if (choices?.[0]?.message?.tool_calls || choices?.[0]?.message?.toolCalls) {\n return 'openai';\n }\n // Check delta for streaming responses (OpenRouter SDK uses this)\n if (choices?.[0]?.delta?.tool_calls || choices?.[0]?.delta?.toolCalls) {\n return 'openai';\n }\n\n // Anthropic format: has content array with tool_use type\n if (resp.content && Array.isArray(resp.content)) {\n const hasToolUse = (resp.content as Array<{ type?: string }>).some(\n (block) => block.type === 'tool_use'\n );\n if (hasToolUse) {\n return 'anthropic';\n }\n }\n\n // Google format: has candidates[0].content.parts with functionCall\n const candidates = resp.candidates as\n | Array<{ content?: { parts?: Array<{ functionCall?: unknown }> } }>\n | undefined;\n if (candidates?.[0]?.content?.parts) {\n const hasFunctionCall = candidates[0].content.parts.some((part) => part.functionCall);\n if (hasFunctionCall) {\n return 'google';\n }\n }\n\n return null;\n}\n\n/**\n * Parse tool calls with auto-detection or explicit format\n *\n * A unified parser that can either auto-detect the response format\n * or use a specified format. This is useful when you're not sure\n * which provider's response you're handling.\n *\n * @param response - The raw response from a provider API\n * @param format - The format to use: 'openai', 'anthropic', 'google', or 'auto'\n * @returns Array of parsed tool call requests\n *\n * @example\n * ```typescript\n * // Auto-detect format\n * const toolCalls = parseToolCalls(response, 'auto');\n *\n * // Or specify format explicitly\n * const toolCalls = parseToolCalls(response, 'openai');\n * ```\n */\nexport function parseToolCalls(\n response: unknown,\n format: ToolCallFormat = 'auto'\n): ToolCallRequest[] {\n let actualFormat: ToolCallFormat | null = format;\n\n if (format === 'auto') {\n actualFormat = detectToolCallFormat(response);\n if (!actualFormat) {\n return [];\n }\n }\n\n switch (actualFormat) {\n case 'openai':\n return parseOpenAIToolCalls(response);\n case 'anthropic':\n return parseAnthropicToolCalls(response);\n case 'google':\n return parseGoogleToolCalls(response);\n default:\n return [];\n }\n}\n\n/**\n * Check if a response contains tool calls\n *\n * Quick check to determine if a response has any tool calls\n * without fully parsing them.\n *\n * @param response - The raw response from a provider API\n * @returns True if the response contains tool calls\n */\nexport function hasToolCalls(response: unknown): boolean {\n const format = detectToolCallFormat(response);\n return format !== null;\n}\n","/**\n * Tool Format Converters\n *\n * Utilities for converting between different provider tool formats.\n * The universal format (OpenAI-style) serves as the baseline for all conversions.\n *\n * @module @quilltap/plugin-utils/tools/converters\n */\n\nimport type {\n UniversalTool,\n AnthropicToolDefinition,\n GoogleToolDefinition,\n OpenAIToolDefinition,\n} from '@quilltap/plugin-types';\n\n/**\n * Convert OpenAI/Universal format tool to Anthropic format\n *\n * Anthropic uses a tool_use format with:\n * - name: string\n * - description: string\n * - input_schema: JSON schema object\n *\n * @param tool - Universal tool or OpenAI tool definition\n * @returns Tool formatted for Anthropic's tool_use\n *\n * @example\n * ```typescript\n * const anthropicTool = convertToAnthropicFormat(universalTool);\n * // Returns: {\n * // name: 'search_web',\n * // description: 'Search the web',\n * // input_schema: {\n * // type: 'object',\n * // properties: { query: { type: 'string' } },\n * // required: ['query']\n * // }\n * // }\n * ```\n */\nexport function convertToAnthropicFormat(tool: UniversalTool | OpenAIToolDefinition): AnthropicToolDefinition {\n return {\n name: tool.function.name,\n description: tool.function.description ?? '',\n input_schema: {\n type: 'object',\n properties: tool.function.parameters?.properties ?? {},\n required: tool.function.parameters?.required ?? [],\n },\n };\n}\n\n/**\n * Convert OpenAI/Universal format tool to Google Gemini format\n *\n * Google uses a function calling format with:\n * - name: string\n * - description: string\n * - parameters: JSON schema object\n *\n * @param tool - Universal tool or OpenAI tool definition\n * @returns Tool formatted for Google's functionCall\n *\n * @example\n * ```typescript\n * const googleTool = convertToGoogleFormat(universalTool);\n * // Returns: {\n * // name: 'search_web',\n * // description: 'Search the web',\n * // parameters: {\n * // type: 'object',\n * // properties: { query: { type: 'string' } },\n * // required: ['query']\n * // }\n * // }\n * ```\n */\nexport function convertToGoogleFormat(tool: UniversalTool | OpenAIToolDefinition): GoogleToolDefinition {\n return {\n name: tool.function.name,\n description: tool.function.description ?? '',\n parameters: {\n type: 'object',\n properties: tool.function.parameters?.properties ?? {},\n required: tool.function.parameters?.required ?? [],\n },\n };\n}\n\n/**\n * Convert Anthropic format tool to Universal/OpenAI format\n *\n * @param tool - Anthropic format tool\n * @returns Tool in universal OpenAI format\n */\nexport function convertFromAnthropicFormat(tool: AnthropicToolDefinition): UniversalTool {\n return {\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description ?? '',\n parameters: {\n type: 'object',\n properties: tool.input_schema.properties,\n required: tool.input_schema.required ?? [],\n },\n },\n };\n}\n\n/**\n * Convert Google format tool to Universal/OpenAI format\n *\n * @param tool - Google format tool\n * @returns Tool in universal OpenAI format\n */\nexport function convertFromGoogleFormat(tool: GoogleToolDefinition): UniversalTool {\n return {\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object',\n properties: tool.parameters.properties,\n required: tool.parameters.required,\n },\n },\n };\n}\n\n/**\n * Apply prompt/description length limit to a tool\n *\n * Modifies a tool's description if it exceeds maxBytes, appending a warning\n * that the description was truncated. This is useful for providers with\n * strict token limits.\n *\n * @param tool - Tool object (any format) with a description property\n * @param maxBytes - Maximum bytes allowed for description (including warning)\n * @returns Modified tool with truncated description if needed\n *\n * @example\n * ```typescript\n * const limitedTool = applyDescriptionLimit(tool, 500);\n * // If description > 500 bytes, truncates and adds warning\n * ```\n */\nexport function applyDescriptionLimit<T extends { description: string }>(\n tool: T,\n maxBytes: number\n): T {\n if (!tool || !tool.description) {\n return tool;\n }\n\n const warningText = ' [Note: description truncated due to length limit]';\n const maxDescBytes = maxBytes - Buffer.byteLength(warningText);\n\n if (maxDescBytes <= 0) {\n console.warn('[plugin-utils] Length limit too small for warning text:', maxBytes);\n return tool;\n }\n\n const descBytes = Buffer.byteLength(tool.description);\n\n if (descBytes > maxBytes) {\n // Truncate description to fit within the byte limit\n let truncated = tool.description;\n while (Buffer.byteLength(truncated) > maxDescBytes && truncated.length > 0) {\n truncated = truncated.slice(0, -1);\n }\n\n return {\n ...tool,\n description: truncated + warningText,\n };\n }\n\n return tool;\n}\n\n/**\n * Target format for tool conversion\n */\nexport type ToolConvertTarget = 'openai' | 'anthropic' | 'google';\n\n/**\n * Convert a universal tool to a specific provider format\n *\n * @param tool - Universal tool or OpenAI tool definition\n * @param target - Target provider format\n * @returns Tool in the target format\n */\nexport function convertToolTo(\n tool: UniversalTool | OpenAIToolDefinition,\n target: ToolConvertTarget\n): UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition {\n switch (target) {\n case 'anthropic':\n return convertToAnthropicFormat(tool);\n case 'google':\n return convertToGoogleFormat(tool);\n case 'openai':\n default:\n return tool;\n }\n}\n\n/**\n * Convert multiple tools to a specific provider format\n *\n * @param tools - Array of universal tools or OpenAI tool definitions\n * @param target - Target provider format\n * @returns Array of tools in the target format\n */\nexport function convertToolsTo(\n tools: Array<UniversalTool | OpenAIToolDefinition>,\n target: ToolConvertTarget\n): Array<UniversalTool | OpenAIToolDefinition | AnthropicToolDefinition | GoogleToolDefinition> {\n return tools.map((tool) => convertToolTo(tool, target));\n}\n\n// ============================================================================\n// Backward-compatible aliases\n// ============================================================================\n\n/**\n * Alias for convertToAnthropicFormat\n * @deprecated Use convertToAnthropicFormat instead\n */\nexport const convertOpenAIToAnthropicFormat = convertToAnthropicFormat;\n\n/**\n * Alias for convertToGoogleFormat\n * @deprecated Use convertToGoogleFormat instead\n */\nexport const convertOpenAIToGoogleFormat = convertToGoogleFormat;\n","/**\n * Plugin Logger Bridge\n *\n * Provides a logger factory for plugins that automatically bridges\n * to Quilltap's core logging when running inside the host application,\n * or falls back to console logging when running standalone.\n *\n * @module @quilltap/plugin-utils/logging/plugin-logger\n */\n\nimport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n/**\n * Extended logger interface with child logger support\n */\nexport interface PluginLoggerWithChild extends PluginLogger {\n /**\n * Create a child logger with additional context\n * @param additionalContext Context to merge with parent context\n * @returns A new logger with combined context\n */\n child(additionalContext: LogContext): PluginLoggerWithChild;\n}\n\n/**\n * Type for the global Quilltap logger bridge\n * Stored on globalThis to work across different npm package copies\n */\ndeclare global {\n \n var __quilltap_logger_factory:\n | ((pluginName: string) => PluginLoggerWithChild)\n | undefined;\n}\n\n/**\n * Get the core logger factory from global namespace\n *\n * @returns The injected factory or null if not in Quilltap environment\n */\nfunction getCoreLoggerFactory(): ((pluginName: string) => PluginLoggerWithChild) | null {\n return globalThis.__quilltap_logger_factory ?? null;\n}\n\n/**\n * Inject the core logger factory from Quilltap host\n *\n * This is called by Quilltap core when loading plugins to bridge\n * plugin logging into the host's logging system. Uses globalThis\n * to ensure it works even when plugins have their own copy of\n * plugin-utils in their node_modules.\n *\n * **Internal API - not for plugin use**\n *\n * @param factory A function that creates a child logger for a plugin\n */\nexport function __injectCoreLoggerFactory(\n factory: (pluginName: string) => PluginLoggerWithChild\n): void {\n globalThis.__quilltap_logger_factory = factory;\n}\n\n/**\n * Clear the injected core logger factory\n *\n * Useful for testing or when unloading the plugin system.\n *\n * **Internal API - not for plugin use**\n */\nexport function __clearCoreLoggerFactory(): void {\n globalThis.__quilltap_logger_factory = undefined;\n}\n\n/**\n * Check if a core logger has been injected\n *\n * @returns True if running inside Quilltap with core logging available\n */\nexport function hasCoreLogger(): boolean {\n return getCoreLoggerFactory() !== null;\n}\n\n/**\n * Create a console logger with child support\n *\n * @param prefix Logger prefix\n * @param minLevel Minimum log level\n * @param baseContext Base context to include in all logs\n */\nfunction createConsoleLoggerWithChild(\n prefix: string,\n minLevel: LogLevel = 'debug',\n baseContext: LogContext = {}\n): PluginLoggerWithChild {\n const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];\n const shouldLog = (level: LogLevel): boolean =>\n levels.indexOf(level) >= levels.indexOf(minLevel);\n\n const formatContext = (context?: LogContext): string => {\n const merged = { ...baseContext, ...context };\n const entries = Object.entries(merged)\n .filter(([key]) => key !== 'context')\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ');\n return entries ? ` ${entries}` : '';\n };\n\n const logger: PluginLoggerWithChild = {\n debug: (message: string, context?: LogContext): void => {\n if (shouldLog('debug')) {\n console.debug(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n info: (message: string, context?: LogContext): void => {\n if (shouldLog('info')) {\n console.info(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n warn: (message: string, context?: LogContext): void => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}] ${message}${formatContext(context)}`);\n }\n },\n\n error: (message: string, context?: LogContext, error?: Error): void => {\n if (shouldLog('error')) {\n console.error(\n `[${prefix}] ${message}${formatContext(context)}`,\n error ? `\\n${error.stack || error.message}` : ''\n );\n }\n },\n\n child: (additionalContext: LogContext): PluginLoggerWithChild => {\n return createConsoleLoggerWithChild(prefix, minLevel, {\n ...baseContext,\n ...additionalContext,\n });\n },\n };\n\n return logger;\n}\n\n/**\n * Create a plugin logger that bridges to Quilltap core logging\n *\n * When running inside Quilltap:\n * - Routes all logs to the core logger\n * - Tags logs with `{ plugin: pluginName, module: 'plugin' }`\n * - Logs appear in Quilltap's combined.log and console\n *\n * When running standalone:\n * - Falls back to console logging with `[pluginName]` prefix\n * - Respects the specified minimum log level\n *\n * @param pluginName - The plugin identifier (e.g., 'qtap-plugin-openai')\n * @param minLevel - Minimum log level when running standalone (default: 'debug')\n * @returns A logger instance\n *\n * @example\n * ```typescript\n * // In your plugin's provider.ts\n * import { createPluginLogger } from '@quilltap/plugin-utils';\n *\n * const logger = createPluginLogger('qtap-plugin-my-provider');\n *\n * export class MyProvider implements LLMProvider {\n * async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n * logger.debug('Sending message', { model: params.model });\n *\n * try {\n * const response = await this.client.chat({...});\n * logger.info('Received response', { tokens: response.usage?.total_tokens });\n * return response;\n * } catch (error) {\n * logger.error('Failed to send message', { model: params.model }, error as Error);\n * throw error;\n * }\n * }\n * }\n * ```\n */\nexport function createPluginLogger(\n pluginName: string,\n minLevel: LogLevel = 'debug'\n): PluginLoggerWithChild {\n // Check for core logger factory from global namespace\n const coreFactory = getCoreLoggerFactory();\n if (coreFactory) {\n return coreFactory(pluginName);\n }\n\n // Standalone mode: use enhanced console logger\n return createConsoleLoggerWithChild(pluginName, minLevel);\n}\n\n/**\n * Get the minimum log level from environment\n *\n * Checks for LOG_LEVEL or QUILLTAP_LOG_LEVEL environment variables.\n * Useful for configuring standalone plugin logging.\n *\n * @returns The configured log level, or 'info' as default\n */\nexport function getLogLevelFromEnv(): LogLevel {\n if (typeof process !== 'undefined' && process.env) {\n const envLevel = process.env.LOG_LEVEL || process.env.QUILLTAP_LOG_LEVEL;\n if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) {\n return envLevel as LogLevel;\n }\n }\n return 'info';\n}\n","/**\n * Logging Utilities\n *\n * Exports the plugin logger bridge and related utilities.\n *\n * @module @quilltap/plugin-utils/logging\n */\n\nexport {\n createPluginLogger,\n hasCoreLogger,\n getLogLevelFromEnv,\n __injectCoreLoggerFactory,\n __clearCoreLoggerFactory,\n} from './plugin-logger';\n\nexport type { PluginLoggerWithChild } from './plugin-logger';\n\n// Re-export logger types from plugin-types\nexport type { PluginLogger, LogContext, LogLevel } from '@quilltap/plugin-types';\n\n// Re-export logger utilities from plugin-types for convenience\nexport { createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';\n","/**\n * OpenAI-Compatible Provider Base Class\n *\n * A reusable base class for building LLM providers that use OpenAI-compatible APIs.\n * This includes services like:\n * - Local LLM servers (LM Studio, vLLM, Text Generation Web UI, Ollama with OpenAI compat)\n * - Cloud services with OpenAI-compatible APIs (Gab AI, Together AI, Fireworks, etc.)\n *\n * External plugins can extend this class to create custom providers with minimal code:\n *\n * @example\n * ```typescript\n * import { OpenAICompatibleProvider } from '@quilltap/plugin-utils';\n *\n * export class MyCustomProvider extends OpenAICompatibleProvider {\n * constructor() {\n * super({\n * baseUrl: 'https://api.my-service.com/v1',\n * providerName: 'MyService',\n * requireApiKey: true,\n * attachmentErrorMessage: 'MyService does not support file attachments',\n * });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport OpenAI from 'openai';\nimport type {\n TextProvider,\n LLMParams,\n LLMResponse,\n StreamChunk,\n PluginLogger,\n} from '@quilltap/plugin-types';\nimport { createPluginLogger } from '../logging';\nimport { getQuilltapUserAgent } from '../version';\nimport { DEFAULT_REQUEST_TIMEOUT_MS, buildSdkRequestOptions } from './request-budget';\nimport { applyProfileParameters } from './profile-parameters';\nimport { collapseLeadingSystemMessages } from './system-messages';\n\n/** Matches the OpenAI SDK default; retries never apply to a caller-supplied budget. */\nconst DEFAULT_MAX_RETRIES = 2;\n\n/**\n * Configuration options for OpenAI-compatible providers.\n *\n * Use this interface when extending OpenAICompatibleProvider to customize\n * the provider's behavior for your specific service.\n */\nexport interface OpenAICompatibleProviderConfig {\n /**\n * Base URL for the API endpoint.\n * Should include the version path (e.g., 'https://api.example.com/v1')\n */\n baseUrl: string;\n\n /**\n * Provider name used for logging context.\n * This appears in log messages to identify which provider generated them.\n * @default 'OpenAICompatible'\n */\n providerName?: string;\n\n /**\n * Whether an API key is required for this provider.\n * If true, requests will fail with an error when no API key is provided.\n * If false, requests will use 'not-needed' as the API key (for local servers).\n * @default false\n */\n requireApiKey?: boolean;\n\n /**\n * Custom error message shown when file attachments are attempted.\n * @default 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)'\n */\n attachmentErrorMessage?: string;\n\n /**\n * Default wall-clock budget per request, in milliseconds.\n *\n * The OpenAI SDK's own default is 600 000 ms, which means a service that\n * accepts a connection and then goes quiet holds the caller for ten minutes\n * before the first retry. That is far longer than any Quilltap caller wants\n * to wait, so the base class lowers it.\n *\n * Individual requests can tighten this further via `LLMParams.requestTimeoutMs`.\n *\n * @default 300000 (5 minutes)\n */\n requestTimeoutMs?: number;\n\n /**\n * How many times the SDK may retry a failed or timed-out request.\n *\n * Applies only to requests that did *not* specify their own\n * `LLMParams.requestTimeoutMs` — a caller-supplied budget is a hard ceiling\n * and never retries.\n *\n * @default 2 (the OpenAI SDK default)\n */\n maxRetries?: number;\n}\n\n/**\n * Base provider class for OpenAI-compatible APIs.\n *\n * This class implements the full LLMProvider interface using the OpenAI SDK,\n * allowing subclasses to create custom providers with just configuration.\n *\n * Features:\n * - Streaming and non-streaming chat completions\n * - API key validation\n * - Model listing\n * - Configurable API key requirements\n * - Dynamic logging with provider name context\n *\n * @remarks\n * File attachments and image generation are not supported by default,\n * as support varies across OpenAI-compatible implementations.\n */\nexport class OpenAICompatibleProvider implements TextProvider {\n /** File attachments are not supported by default */\n readonly supportsFileAttachments = false;\n /** No MIME types are supported for attachments */\n readonly supportedMimeTypes: string[] = [];\n /** Web search is not supported */\n readonly supportsWebSearch = false;\n\n /** Base URL for the API endpoint */\n protected readonly baseUrl: string;\n /** Provider name for logging */\n protected readonly providerName: string;\n /** Whether API key is required */\n protected readonly requireApiKey: boolean;\n /** Error message for attachment failures */\n protected readonly attachmentErrorMessage: string;\n /** Default per-request wall-clock budget in milliseconds */\n protected readonly requestTimeoutMs: number;\n /** Retry count for requests that carry no caller-supplied budget */\n protected readonly maxRetries: number;\n /** Logger instance */\n protected readonly logger: PluginLogger;\n\n /**\n * Whether the endpoint tolerates more than one leading `system` message.\n *\n * True for every hosted service — and so the default, which keeps the\n * outbound bytes of OpenRouter, Z.AI and every other subclass exactly as they\n * were. A subclass that points at a local runtime applying the *model's* chat\n * template sets this false: several template families reject a system message\n * anywhere but index 0, and Quilltap's context builder deliberately emits up\n * to three of them so its cache breakpoints survive. See Bug 82 and\n * {@link collapseLeadingSystemMessages}.\n */\n protected readonly acceptsRepeatedSystemMessages: boolean = true;\n\n /**\n * Apply {@link acceptsRepeatedSystemMessages} to a mapped wire-message array.\n * Called by both body builds so the streaming and non-streaming shapes cannot\n * drift apart.\n */\n protected applySystemMessagePolicy<T extends { role: string; content?: string | null }>(\n messages: T[]\n ): T[] {\n return this.acceptsRepeatedSystemMessages ? messages : collapseLeadingSystemMessages(messages);\n }\n\n /**\n * Creates a new OpenAI-compatible provider instance.\n *\n * @param config - Configuration object or base URL string (for backward compatibility)\n */\n constructor(config: string | OpenAICompatibleProviderConfig) {\n // Support both legacy string baseUrl and new config object\n if (typeof config === 'string') {\n this.baseUrl = config;\n this.providerName = 'OpenAICompatible';\n this.requireApiKey = false;\n this.attachmentErrorMessage =\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = DEFAULT_MAX_RETRIES;\n } else {\n this.baseUrl = config.baseUrl;\n this.providerName = config.providerName ?? 'OpenAICompatible';\n this.requireApiKey = config.requireApiKey ?? false;\n this.attachmentErrorMessage =\n config.attachmentErrorMessage ??\n 'OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)';\n this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n }\n\n this.logger = createPluginLogger(`${this.providerName}Provider`);\n }\n\n /**\n * Collects attachment failures for messages with attachments.\n * Since attachments are not supported, all attachments are marked as failed.\n *\n * @param params - LLM parameters containing messages\n * @returns Object with empty sent array and failed attachments\n */\n protected collectAttachmentFailures(\n params: LLMParams\n ): { sent: string[]; failed: { id: string; error: string }[] } {\n const failed: { id: string; error: string }[] = [];\n for (const msg of params.messages) {\n if (msg.attachments) {\n for (const attachment of msg.attachments) {\n failed.push({\n id: attachment.id,\n error: this.attachmentErrorMessage,\n });\n }\n }\n }\n return { sent: [], failed };\n }\n\n /**\n * Validates that an API key is provided when required.\n * @throws Error if API key is required but not provided\n */\n protected validateApiKeyRequirement(apiKey: string): void {\n if (this.requireApiKey && !apiKey) {\n throw new Error(`${this.providerName} provider requires an API key`);\n }\n }\n\n /**\n * Gets the effective API key to use for requests.\n * Returns 'not-needed' for providers that don't require keys.\n */\n protected getEffectiveApiKey(apiKey: string): string {\n return this.requireApiKey ? apiKey : apiKey || 'not-needed';\n }\n\n /**\n * Creates an OpenAI client configured with the provider's base URL,\n * API key, and Quilltap User-Agent header.\n *\n * @param apiKey - API key for authentication\n * @returns Configured OpenAI client instance\n */\n protected createClient(apiKey: string): OpenAI {\n return new OpenAI({\n apiKey: this.getEffectiveApiKey(apiKey),\n baseURL: this.baseUrl,\n defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n timeout: this.requestTimeoutMs,\n maxRetries: this.maxRetries,\n });\n }\n\n /**\n * Builds the per-request options for a single call.\n *\n * When the caller supplied `params.requestTimeoutMs` it is a hard ceiling:\n * the request uses that budget and does not retry, so the total time spent is\n * what the caller asked for rather than a multiple of it. Otherwise the\n * client-level defaults apply.\n *\n * Subclasses that build their own request bodies should pass the result as\n * the second argument to `client.chat.completions.create(...)`.\n *\n * @param params - LLM parameters for the request\n * @returns Request options for the OpenAI SDK, or undefined to use defaults\n */\n protected buildRequestOptions(\n params: LLMParams\n ): { timeout: number; maxRetries: number } | undefined {\n return buildSdkRequestOptions(params);\n }\n\n /**\n * Profile-parameter keys this provider forwards from\n * `LLMParams.profileParameters` onto the request body.\n *\n * **Empty by default**, so a subclass that declares nothing sends exactly the\n * bytes it sent before this mechanism existed. Subclasses opt in by\n * overriding with their own allow-list; `model`, `messages`, `stream`,\n * `stream_options` and `tools` must never appear in one.\n */\n protected readonly profileParamAllowlist: readonly string[] = [];\n\n /**\n * Per-key hook for allow-listed profile parameters. The default forwards the\n * stored value verbatim.\n *\n * Override to reshape a value (the editor stores flat strings where some APIs\n * want objects), to gate a key on the model, or — by writing into `body` and\n * returning `undefined` — to send it under a different key entirely.\n */\n protected normalizeProfileParam(\n _key: string,\n value: unknown,\n _params: LLMParams,\n _body: Record<string, unknown>\n ): unknown | undefined {\n return value;\n }\n\n /**\n * Apply {@link profileParamAllowlist} to a request body under construction.\n * Called by both body builds so the streaming and non-streaming shapes cannot\n * drift apart.\n */\n protected applyProfileParams(body: Record<string, unknown>, params: LLMParams): void {\n applyProfileParameters(\n body,\n params,\n this.profileParamAllowlist,\n (key, value, callParams, target) =>\n this.normalizeProfileParam(key, value, callParams, target)\n );\n }\n\n /**\n * Normalize an OpenAI-shaped `tool_calls` array into Quilltap's `ToolCall`\n * shape, tolerating servers that hand back already-parsed argument objects\n * where the OpenAI wire format specifies a JSON string.\n */\n protected normalizeToolCalls(\n toolCalls: readonly unknown[] | undefined | null\n ): { id: string; type: 'function'; function: { name: string; arguments: string } }[] {\n if (!toolCalls || toolCalls.length === 0) return [];\n const out: { id: string; type: 'function'; function: { name: string; arguments: string } }[] = [];\n for (const raw of toolCalls) {\n const tc = raw as {\n id?: string;\n function?: { name?: string; arguments?: unknown };\n };\n if (!tc?.function?.name) continue;\n const args = tc.function.arguments;\n out.push({\n id: tc.id ?? '',\n type: 'function',\n function: {\n name: tc.function.name,\n arguments: typeof args === 'string' ? args : JSON.stringify(args ?? {}),\n },\n });\n }\n return out;\n }\n\n /**\n * Build the Chat Completions request body — the ONE build both\n * {@link sendMessage} and {@link streamMessage} call, so the streaming and\n * non-streaming shapes cannot drift apart. Maps Quilltap messages to the\n * OpenAI wire format (tool results and assistant tool calls included),\n * applies {@link applySystemMessagePolicy}, assembles the body literal, and\n * finishes with {@link applyProfileParams}.\n *\n * Subclasses that keep the base class's send/stream loops can override this\n * to reshape the body in one place.\n *\n * @param params - LLM parameters for the request\n * @param stream - Whether the body is for a streaming request; adds\n * `stream: true` and `stream_options: { include_usage: true }`\n */\n protected buildRequestBody(params: LLMParams, stream: boolean): Record<string, unknown> {\n // Map messages to OpenAI Chat Completions format, including tool messages\n const mappedMessages = params.messages\n .filter((m) => {\n // Skip tool messages without toolCallId (backward compat)\n if (m.role === 'tool' && !m.toolCallId) return false;\n return true;\n })\n .map((m) => {\n // Tool result messages\n if (m.role === 'tool' && m.toolCallId) {\n return {\n role: 'tool' as const,\n tool_call_id: m.toolCallId as string,\n content: m.content,\n };\n }\n // Assistant messages with tool calls\n if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: 'assistant' as const,\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc: any) => ({\n id: tc.id,\n type: tc.type,\n function: tc.function,\n })),\n };\n }\n // Standard messages (strip attachments)\n return {\n role: m.role as 'system' | 'user' | 'assistant',\n content: m.content,\n };\n });\n\n // Endpoints whose chat template insists the system message be first and\n // singular get the leading run folded into one (Bug 82); everyone else is\n // handed the array untouched.\n const messages = this.applySystemMessagePolicy(mappedMessages);\n\n // Built as a Record so the allow-list can write non-standard keys the\n // OpenAI SDK's own types don't know about (`chat_template_kwargs`, `top_k`,\n // …). Widening happens once, here, rather than as `as any` at each write.\n const body: Record<string, unknown> = {\n model: params.model,\n messages,\n temperature: params.temperature ?? 0.7,\n max_tokens: params.maxTokens ?? 4096,\n top_p: params.topP ?? 1,\n stop: params.stop,\n // Spread here — not appended after the literal — so the streaming keys\n // keep their historical position between `stop` and `user` and the\n // serialized body stays byte-identical.\n ...(stream ? { stream: true, stream_options: { include_usage: true } } : {}),\n ...(params.cacheKey ? { user: params.cacheKey } : {}),\n // Tools arrive only once the runtime gate (the profile's \"Allow tool\n // use\") has passed, so no separate guard is needed here.\n ...(params.tools && params.tools.length > 0\n ? { tools: params.tools, tool_choice: params.toolChoice ?? 'auto' }\n : {}),\n };\n this.applyProfileParams(body, params);\n return body;\n }\n\n /**\n * Normalizes a Chat Completions `usage` block into Quilltap's shape.\n *\n * OpenAI-compatible endpoints fold prompt-cache hits into `prompt_tokens`\n * (reported separately as `prompt_tokens_details.cached_tokens`). Quilltap's\n * convention is that `promptTokens` / `totalTokens` EXCLUDE cache reads, so\n * cached input never counts against token budgets; `cacheUsage` still\n * carries the cache reads for display. Endpoints that report no\n * `cached_tokens` are unaffected.\n */\n protected normalizeUsage(\n usage: OpenAI.Completions.CompletionUsage | null | undefined\n ): Pick<LLMResponse, 'usage' | 'cacheUsage'> {\n const cachedTokens = usage?.prompt_tokens_details?.cached_tokens ?? 0;\n if (cachedTokens > 0) {\n this.logger.debug(`${this.providerName} usage: excluding prompt-cache hits from prompt/total`, {\n context: `${this.providerName}Provider.normalizeUsage`,\n promptTokens: usage?.prompt_tokens ?? 0,\n cachedTokens,\n });\n }\n return {\n usage: {\n promptTokens: Math.max(0, (usage?.prompt_tokens ?? 0) - cachedTokens),\n completionTokens: usage?.completion_tokens ?? 0,\n totalTokens: Math.max(0, (usage?.total_tokens ?? 0) - cachedTokens),\n },\n ...(cachedTokens > 0\n ? { cacheUsage: { cacheReadInputTokens: cachedTokens, cachedTokens } }\n : {}),\n };\n }\n\n /**\n * Sends a message and returns the complete response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @returns Complete LLM response with content and usage statistics\n */\n async sendMessage(params: LLMParams, apiKey: string): Promise<LLMResponse> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n const body = this.buildRequestBody(params, false);\n\n try {\n const response = (await client.chat.completions.create(\n body as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,\n this.buildRequestOptions(params)\n )) as OpenAI.Chat.Completions.ChatCompletion;\n\n const choice = response.choices[0];\n const toolCalls = this.normalizeToolCalls(choice.message.tool_calls);\n return {\n content: choice.message.content ?? '',\n finishReason: choice.finish_reason,\n ...this.normalizeUsage(response.usage),\n raw: response,\n attachmentResults,\n ...(toolCalls.length > 0 ? { toolCalls } : {}),\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in sendMessage`,\n { context: `${this.providerName}Provider.sendMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Sends a message and streams the response.\n *\n * @param params - LLM parameters including messages, model, and settings\n * @param apiKey - API key for authentication\n * @yields Stream chunks with content and final usage statistics\n */\n async *streamMessage(params: LLMParams, apiKey: string): AsyncGenerator<StreamChunk> {\n this.validateApiKeyRequirement(apiKey);\n const attachmentResults = this.collectAttachmentFailures(params);\n\n const client = this.createClient(apiKey);\n const body = this.buildRequestBody(params, true);\n\n try {\n const stream = (await client.chat.completions.create(\n body as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming,\n this.buildRequestOptions(params)\n )) as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>;\n\n let chunkCount = 0;\n\n // Track usage - it may come in a separate final chunk\n let accumulatedUsage: OpenAI.Completions.CompletionUsage | null = null;\n // Tool-call fragments arrive index-keyed, with `arguments` split across\n // deltas as raw string pieces; accumulate per index and assemble at the\n // end rather than trying to parse mid-stream.\n const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>();\n let finishReason: string | null = null;\n\n for await (const chunk of stream) {\n chunkCount++;\n const choice = chunk.choices[0];\n const content = choice?.delta?.content;\n const hasUsage = chunk.usage;\n\n // Track usage when we get it (may come in a separate final chunk)\n if (hasUsage) {\n accumulatedUsage = chunk.usage ?? null;\n }\n\n if (choice?.delta?.tool_calls) {\n for (const tcDelta of choice.delta.tool_calls) {\n const idx = tcDelta.index ?? 0;\n const existing = toolCallAccumulator.get(idx) ?? { id: '', name: '', arguments: '' };\n if (tcDelta.id) existing.id = tcDelta.id;\n if (tcDelta.function?.name) existing.name = tcDelta.function.name;\n if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;\n toolCallAccumulator.set(idx, existing);\n }\n }\n\n if (choice?.finish_reason) finishReason = choice.finish_reason;\n\n // Yield content chunks\n if (content) {\n yield {\n content,\n done: false,\n };\n }\n }\n\n const toolCalls = Array.from(toolCallAccumulator.values())\n .filter((tc) => tc.name)\n .map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: { name: tc.name, arguments: tc.arguments },\n }));\n\n // After stream ends, yield final chunk with accumulated usage\n yield {\n content: '',\n done: true,\n ...(accumulatedUsage\n ? {\n ...this.normalizeUsage(accumulatedUsage),\n rawProviderUsage: accumulatedUsage as unknown as Record<string, unknown>,\n }\n : {}),\n attachmentResults,\n // Only synthesized when the stream actually carried tool calls: the\n // host's tool detection reads `rawResponse`, and an empty one on every\n // ordinary turn would be noise in the logs for no gain.\n ...(toolCalls.length > 0\n ? {\n toolCalls,\n rawResponse: {\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: '', tool_calls: toolCalls },\n finish_reason: finishReason,\n },\n ],\n },\n }\n : {}),\n };\n } catch (error) {\n this.logger.error(\n `${this.providerName} API error in streamMessage`,\n { context: `${this.providerName}Provider.streamMessage`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n throw error;\n }\n }\n\n /**\n * Validates an API key by attempting to list models.\n *\n * @param apiKey - API key to validate\n * @returns true if the API key is valid, false otherwise\n */\n async validateApiKey(apiKey: string): Promise<boolean> {\n // For providers that require API key, return false if not provided\n if (this.requireApiKey && !apiKey) {\n return false;\n }\n\n try {\n const client = this.createClient(apiKey);\n await client.models.list();\n return true;\n } catch (error) {\n this.logger.error(\n `${this.providerName} API validation failed`,\n { context: `${this.providerName}Provider.validateApiKey`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return false;\n }\n }\n\n /**\n * Fetches available models from the API.\n *\n * @param apiKey - API key for authentication\n * @returns Sorted array of model IDs, or empty array on failure\n */\n async getAvailableModels(apiKey: string): Promise<string[]> {\n // For providers that require API key, return empty if not provided\n if (this.requireApiKey && !apiKey) {\n this.logger.error(`${this.providerName} provider requires an API key to fetch models`, {\n context: `${this.providerName}Provider.getAvailableModels`,\n });\n return [];\n }\n\n try {\n const client = this.createClient(apiKey);\n const models = await client.models.list();\n const modelList = models.data.map((m) => m.id).sort();\n return modelList;\n } catch (error) {\n this.logger.error(\n `Failed to fetch ${this.providerName} models`,\n { context: `${this.providerName}Provider.getAvailableModels`, baseUrl: this.baseUrl },\n error instanceof Error ? error : undefined\n );\n return [];\n }\n }\n\n}\n","/**\n * Quilltap Version Management\n *\n * Provides access to the Quilltap application version for plugins.\n * Uses the same globalThis injection pattern as the logger factory,\n * allowing the host app to inject its version at startup.\n *\n * @module @quilltap/plugin-utils/version\n */\n\n/**\n * Global key used to store the injected Quilltap version.\n * Must match the key used by the host app's injection code.\n */\nconst GLOBAL_VERSION_KEY = '__quilltap_app_version';\n\n/**\n * Inject the Quilltap application version into the global namespace.\n *\n * This should be called early in plugin initialization (alongside the\n * logger factory injection), before any plugins are loaded. Plugins\n * can then retrieve the version via `getQuilltapVersion()`.\n *\n * @param version - The Quilltap application version string (e.g., '3.3.0')\n *\n * @example\n * ```typescript\n * import { __injectQuilltapVersion } from '@quilltap/plugin-utils';\n * import packageJson from './package.json';\n *\n * __injectQuilltapVersion(packageJson.version);\n * ```\n */\nexport function __injectQuilltapVersion(version: string): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = version;\n}\n\n/**\n * Clear the injected Quilltap version from the global namespace.\n *\n * Useful for testing or cleanup.\n */\nexport function __clearQuilltapVersion(): void {\n (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY] = undefined;\n}\n\n/**\n * Get the Quilltap application version.\n *\n * Returns the version injected by the host app, or 'unknown' if\n * no version has been injected (e.g., running outside of Quilltap).\n *\n * @returns The Quilltap version string (e.g., '3.3.0') or 'unknown'\n */\nexport function getQuilltapVersion(): string {\n const version = (globalThis as Record<string, unknown>)[GLOBAL_VERSION_KEY];\n return typeof version === 'string' ? version : 'unknown';\n}\n\n/**\n * Get the Quilltap User-Agent string for API requests.\n *\n * Returns a string in the format `Quilltap/{version}` suitable for\n * use as a User-Agent header or app identifier in API calls.\n *\n * @returns User-Agent string (e.g., 'Quilltap/3.3.0')\n *\n * @example\n * ```typescript\n * import { getQuilltapUserAgent } from '@quilltap/plugin-utils';\n *\n * const client = new OpenAI({\n * apiKey,\n * defaultHeaders: { 'User-Agent': getQuilltapUserAgent() },\n * });\n * ```\n */\nexport function getQuilltapUserAgent(): string {\n return `Quilltap/${getQuilltapVersion()}`;\n}\n","/**\n * Per-request wall-clock budgets.\n *\n * Every provider SDK ships a generous default request timeout — the OpenAI and\n * Anthropic SDKs both use ten minutes — on the assumption that a slow answer is\n * better than no answer. That assumption is wrong for work Quilltap awaits\n * inline on a user-visible turn: a service that accepts the connection and then\n * goes silent holds the turn for the full ten minutes before the SDK's first\n * retry, with nothing logged, because request logs are only written when a call\n * finishes.\n *\n * `LLMParams.requestTimeoutMs` is the caller's answer to \"how long is this\n * worth waiting for\". These helpers are the single place that decides what that\n * number means, so an SDK-backed provider and a `fetch`-based one apply it the\n * same way.\n *\n * @packageDocumentation\n */\n\nimport type { LLMParams } from '@quilltap/plugin-types';\n\n/**\n * Fallback budget for a request whose caller expressed no preference.\n *\n * Half the SDK default: long enough for a slow non-streaming generation, short\n * enough that a silently-stalled endpoint fails in minutes rather than tens of\n * minutes.\n */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;\n\n/**\n * The budget for one request, in milliseconds.\n *\n * @param params - LLM parameters, whose `requestTimeoutMs` wins when set\n * @param defaultMs - Budget to use when the caller named none\n * @returns The effective budget in milliseconds\n */\nexport function resolveRequestTimeoutMs(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): number {\n const requested = params.requestTimeoutMs;\n return typeof requested === 'number' && requested > 0 ? requested : defaultMs;\n}\n\n/**\n * Per-request options for an OpenAI- or Anthropic-style SDK call.\n *\n * A caller-supplied budget is a *ceiling*, not a per-attempt allowance, so\n * retries are disabled for it — three attempts at the requested budget would\n * spend three times what the caller agreed to. Returns undefined when the\n * caller named no budget, leaving the client's own defaults in charge.\n *\n * @param params - LLM parameters for the request\n * @returns SDK request options, or undefined to use the client defaults\n */\nexport function buildSdkRequestOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>\n): { timeout: number; maxRetries: number } | undefined {\n const requested = params.requestTimeoutMs;\n if (typeof requested !== 'number' || requested <= 0) return undefined;\n return { timeout: requested, maxRetries: 0 };\n}\n\n/**\n * Client-construction options for a provider that builds a fresh SDK client\n * per call, where \"client default\" and \"this request\" are the same thing.\n *\n * Always defined, unlike {@link buildSdkRequestOptions} — a per-call client has\n * no pre-existing defaults worth preserving, so it takes the resolved budget\n * either way and only forgoes retries when the caller set a ceiling.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns `timeout` and `maxRetries` to spread into the SDK constructor\n */\nexport function buildSdkClientOptions(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): { timeout: number; maxRetries: number } {\n const capped = typeof params.requestTimeoutMs === 'number' && params.requestTimeoutMs > 0;\n return {\n timeout: resolveRequestTimeoutMs(params, defaultMs),\n maxRetries: capped ? 0 : 2,\n };\n}\n\n/**\n * An abort signal that fires at the request's budget, for providers that call\n * `fetch` directly rather than through an SDK.\n *\n * Unlike the SDK options above this bounds the *whole* exchange, body included:\n * `fetch` resolves at the response headers, but the signal stays live while the\n * body streams. Pass a generous `defaultMs` on streaming paths, or compose with\n * the caller's own signal, so a long generation is not cut off mid-answer.\n *\n * @param params - LLM parameters for the request\n * @param defaultMs - Budget to use when the caller named none\n * @returns An AbortSignal that aborts once the budget elapses\n */\nexport function buildRequestAbortSignal(\n params: Pick<LLMParams, 'requestTimeoutMs'>,\n defaultMs: number = DEFAULT_REQUEST_TIMEOUT_MS\n): AbortSignal {\n return AbortSignal.timeout(resolveRequestTimeoutMs(params, defaultMs));\n}\n","/**\n * Profile-parameter passthrough\n *\n * A connection profile's `parameters` blob is free-form JSON: the host stores\n * whatever the profile editor (or a hand-edited profile) puts there and hands\n * it back to the provider as `LLMParams.profileParameters` at call time. What\n * reaches the wire is entirely the provider's business — and a provider that\n * never reads the bag drops every setting in silence, which is exactly the\n * defect Bug 71 records.\n *\n * This is the one mechanism every provider uses to forward those keys.\n *\n * **An exported function rather than a base-class method.** Only one plugin in\n * the repository extends `OpenAICompatibleProvider` (DeepSeek); Z.AI and\n * OpenRouter implement `TextProvider` directly, and Ollama speaks its own\n * REST shape. A `protected` method would have reached one of them, so the\n * mechanism is a free function usable by inheritance *and* composition, and\n * the base class simply calls it.\n *\n * @packageDocumentation\n */\n\nimport type { LLMParams } from '@quilltap/plugin-types';\n\n/**\n * Per-key hook applied to an allow-listed profile parameter on its way into\n * the request body.\n *\n * Return the value to send, or `undefined` to omit the key entirely.\n *\n * The `body` is passed so a normalizer can place a value somewhere other than\n * its own top-level key — the OpenAI-compatible plugin folds `reasoning_effort`\n * into `chat_template_kwargs` this way, then returns `undefined` so no flat\n * `reasoning_effort` key is sent. Normalizers that only reshape a value can\n * ignore the argument.\n */\nexport type ProfileParamNormalizer = (\n key: string,\n value: unknown,\n params: LLMParams,\n body: Record<string, unknown>\n) => unknown | undefined;\n\n/**\n * Copy allow-listed keys from `params.profileParameters` onto a request body.\n *\n * Rules, all of which predate this helper (they were hand-rolled identically in\n * the DeepSeek and Z.AI providers before it existed):\n *\n * - **Allow-list, never spread.** `model`, `messages`, `stream`,\n * `stream_options` and `tools` must be unreachable from a profile — a\n * misconfigured bag cannot be allowed to retarget the request.\n * - **`undefined` and `null` skip.** So does the empty string, which is what\n * the schema-driven profile editor stores for \"omit this and use the model\n * default\".\n * - **The allow-list is ordered.** Keys are applied in the order given, so a\n * normalizer that merges into an earlier key (`chat_template_kwargs`) sees\n * whatever the profile set for it.\n *\n * @param body - Request body under construction; mutated in place\n * @param params - The call's LLM parameters, carrying `profileParameters`\n * @param allowlist - Keys this provider is willing to forward\n * @param normalize - Optional per-key hook (see {@link ProfileParamNormalizer})\n */\nexport function applyProfileParameters(\n body: Record<string, unknown>,\n params: LLMParams,\n allowlist: readonly string[],\n normalize?: ProfileParamNormalizer\n): void {\n const profile = params.profileParameters;\n if (!profile || typeof profile !== 'object') return;\n const bag = profile as Record<string, unknown>;\n\n for (const key of allowlist) {\n const raw = bag[key];\n if (raw === undefined || raw === null) continue;\n // Empty string from the schema-driven profile editor means \"omit the\n // parameter and use the model default.\"\n if (typeof raw === 'string' && raw === '') continue;\n const value = normalize ? normalize(key, raw, params, body) : raw;\n if (value === undefined) continue;\n body[key] = value;\n }\n}\n","/**\n * Leading-system-message collapse.\n *\n * Quilltap's context builder deliberately emits the head of a turn as up to\n * three consecutive `system` messages — the cacheable persona prefix, the\n * static identity reinforcement, and the compressed-history summary — so that a\n * cache breakpoint on the first is not invalidated by churn in the others.\n * Hosted providers accept that happily.\n *\n * A local runtime does not answer for itself: it applies the *model's own* chat\n * template, and several families (Qwen most notably, plus Llama- and\n * Gemma-derived templates) `raise_exception` on any system message after index\n * 0. The whole request is then rejected before a token is generated, so the\n * opening greeting — one system message — works and every turn after it dies.\n *\n * The repair belongs here, at request-build time, rather than in the context\n * assembly: the blocks stay separate for every provider that wants them\n * separate, and the providers that cannot take them fold the run on the way\n * out. See Bug 82.\n *\n * @packageDocumentation\n */\n\n/** The shape this helper needs; wire messages carry more, and keep it. */\ninterface RoleAndContent {\n role: string;\n content?: string | null;\n}\n\n/**\n * Fold a run of consecutive leading `system` messages into a single one,\n * joining their contents with a blank line.\n *\n * Only the *leading* run is touched — a system message that appears later is\n * left where it is, because moving or merging it would change what the model is\n * told and when. Arrays with fewer than two leading system messages are\n * returned unchanged (the same array reference), so a provider that opts in\n * still sends byte-identical requests everywhere the problem does not arise.\n *\n * @param messages - Wire-format messages, already mapped for the endpoint\n * @returns The messages with their leading system run collapsed\n */\nexport function collapseLeadingSystemMessages<T extends RoleAndContent>(messages: T[]): T[] {\n let runLength = 0;\n while (runLength < messages.length && messages[runLength].role === 'system') {\n runLength++;\n }\n\n if (runLength < 2) return messages;\n\n const run = messages.slice(0, runLength);\n const merged: T = {\n ...run[0],\n content: run\n .map((m) => m.content ?? '')\n .filter((c) => c.length > 0)\n .join('\\n\\n'),\n };\n\n return [merged, ...messages.slice(runLength)];\n}\n","/**\n * System Prompt Plugin Builder utilities\n *\n * Provides helper functions for creating and validating system prompt plugins.\n *\n * @module @quilltap/plugin-utils/system-prompts\n */\n\nimport type {\n SystemPromptData,\n SystemPromptMetadata,\n SystemPromptPlugin,\n} from '@quilltap/plugin-types';\n\n// ============================================================================\n// BUILDER OPTIONS\n// ============================================================================\n\n/**\n * Options for creating a system prompt plugin\n */\nexport interface CreateSystemPromptPluginOptions {\n /** Plugin metadata */\n metadata: SystemPromptMetadata;\n\n /**\n * One or more system prompts.\n * Each prompt must have a unique name within the plugin.\n */\n prompts: SystemPromptData[];\n\n /**\n * Optional initialization function.\n * Called when the plugin is loaded.\n */\n initialize?: () => void | Promise<void>;\n}\n\n// ============================================================================\n// BUILDER FUNCTIONS\n// ============================================================================\n\n/**\n * Creates a system prompt plugin.\n *\n * @param options - Plugin configuration options\n * @returns A valid SystemPromptPlugin instance\n *\n * @example\n * ```typescript\n * import { createSystemPromptPlugin } from '@quilltap/plugin-utils';\n *\n * export const plugin = createSystemPromptPlugin({\n * metadata: {\n * pluginId: 'my-prompts',\n * displayName: 'My Prompt Collection',\n * description: 'Custom system prompts for various models',\n * },\n * prompts: [\n * {\n * name: 'CLAUDE_CREATIVE',\n * content: '# Creative Writing Prompt\\n\\nYou are {{char}}...',\n * modelHint: 'CLAUDE',\n * category: 'CREATIVE',\n * },\n * ],\n * });\n * ```\n */\nexport function createSystemPromptPlugin(\n options: CreateSystemPromptPluginOptions\n): SystemPromptPlugin {\n const { metadata, prompts, initialize } = options;\n\n // Validate prompts\n if (prompts.length === 0) {\n throw new Error('At least one system prompt is required');\n }\n\n // Check for duplicate names\n const names = new Set<string>();\n for (const prompt of prompts) {\n if (!prompt.name || prompt.name.trim() === '') {\n throw new Error('Prompt name is required');\n }\n if (!prompt.content || prompt.content.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires content`);\n }\n if (!prompt.modelHint || prompt.modelHint.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a modelHint`);\n }\n if (!prompt.category || prompt.category.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a category`);\n }\n if (names.has(prompt.name)) {\n throw new Error(`Duplicate prompt name: \"${prompt.name}\"`);\n }\n names.add(prompt.name);\n }\n\n // Create the plugin\n const plugin: SystemPromptPlugin = {\n metadata,\n prompts,\n };\n\n // Add initialize function if provided\n if (initialize) {\n plugin.initialize = async () => {\n await initialize();\n };\n }\n\n return plugin;\n}\n\n// ============================================================================\n// VALIDATION UTILITIES\n// ============================================================================\n\n/**\n * Validates a complete system prompt plugin\n *\n * @param plugin - The plugin to validate\n * @returns True if valid, throws Error if invalid\n */\nexport function validateSystemPromptPlugin(plugin: SystemPromptPlugin): boolean {\n // Validate metadata\n if (!plugin.metadata) {\n throw new Error('Plugin metadata is required');\n }\n\n if (!plugin.metadata.pluginId || plugin.metadata.pluginId.trim() === '') {\n throw new Error('Plugin metadata.pluginId is required');\n }\n\n if (!/^[a-z0-9-]+$/.test(plugin.metadata.pluginId)) {\n throw new Error('Plugin pluginId must be lowercase alphanumeric with hyphens only');\n }\n\n if (!plugin.metadata.displayName || plugin.metadata.displayName.trim() === '') {\n throw new Error('Plugin metadata.displayName is required');\n }\n\n // Validate prompts\n if (!plugin.prompts || !Array.isArray(plugin.prompts) || plugin.prompts.length === 0) {\n throw new Error('Plugin must have at least one prompt');\n }\n\n const names = new Set<string>();\n for (const prompt of plugin.prompts) {\n if (!prompt.name || prompt.name.trim() === '') {\n throw new Error('Prompt name is required');\n }\n if (!prompt.content || prompt.content.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires content`);\n }\n if (!prompt.modelHint || prompt.modelHint.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a modelHint`);\n }\n if (!prompt.category || prompt.category.trim() === '') {\n throw new Error(`Prompt \"${prompt.name}\" requires a category`);\n }\n if (names.has(prompt.name)) {\n throw new Error(`Duplicate prompt name: \"${prompt.name}\"`);\n }\n names.add(prompt.name);\n }\n\n return true;\n}\n","/**\n * Built-in tool names for Quilltap\n *\n * These are the tool names used by Quilltap's built-in tools.\n * Plugins that provide dynamic tools (like MCP connectors) should use\n * this list for collision detection to avoid shadowing built-in functionality.\n *\n * @module @quilltap/plugin-utils/builtin-tools\n */\n\n/**\n * Names of all built-in Quilltap tools\n *\n * This set contains the function names of tools that are built into Quilltap:\n * - `generate_image` - AI image generation\n * - `search` - Search the Scriptorium (memories, conversations, backgrounds)\n * - `search_web` - Web search (when enabled)\n * - `project_info` - Get project metadata\n * - `request_full_context` - Request full context reload (context compression)\n * - `help_search` - Search Quilltap help documentation\n * - `help_settings` - Read Quilltap instance settings\n */\nexport const BUILTIN_TOOL_NAMES = new Set([\n 'generate_image',\n 'search',\n 'search_web',\n 'project_info',\n 'request_full_context',\n 'help_search',\n 'help_settings',\n // Document editing tools (Scriptorium Phase 3.3)\n 'doc_read_file',\n 'doc_write_file',\n 'doc_str_replace',\n 'doc_insert_text',\n 'doc_grep',\n 'doc_list_files',\n 'doc_read_frontmatter',\n 'doc_update_frontmatter',\n 'doc_read_heading',\n 'doc_update_heading',\n // Document file management tools (Scriptorium Phase 3.4)\n 'doc_move_file',\n 'doc_copy_file',\n 'doc_delete_file',\n 'doc_create_folder',\n 'doc_delete_folder',\n]);\n\n/**\n * Get the set of built-in tool names\n *\n * Use this function to get the current list of reserved tool names\n * when implementing collision detection in plugins that provide\n * dynamic tools (e.g., MCP server connectors, external tool bridges).\n *\n * @returns Set of tool names that are reserved by Quilltap's built-in tools\n *\n * @example\n * ```typescript\n * import { getBuiltinToolNames } from '@quilltap/plugin-utils';\n *\n * // When generating tool names from external sources:\n * const builtinNames = getBuiltinToolNames();\n * if (builtinNames.has(proposedToolName)) {\n * // Rename or prefix the tool to avoid collision\n * proposedToolName = `external_${proposedToolName}`;\n * }\n * ```\n */\nexport function getBuiltinToolNames(): Set<string> {\n return new Set(BUILTIN_TOOL_NAMES);\n}\n","/**\n * Host URL Rewriting for Container Environments\n *\n * When Quilltap runs inside Docker — or inside a virtual machine the user has\n * built and manages themselves — `localhost` and `127.0.0.1` resolve to the\n * container/VM's own loopback, not the host machine where services like Ollama\n * or LM Studio are running.\n *\n * This module provides a single function that transparently rewrites\n * localhost URLs to point at the host, so users can configure\n * `http://localhost:11434` and have it Just Work in every environment.\n *\n * Gateway resolution order:\n * 1. `QUILLTAP_HOST_IP` env var (explicit override) → rewrite to that IP.\n * This is also the only supported route for a self-managed VM, where\n * Quilltap has no reliable way to detect the host gateway on its own.\n * 2. In Docker: rewrite `localhost` → `host.docker.internal`\n * (Docker Desktop DNS or --add-host on Linux handles the forwarding)\n * 3. Give up gracefully — return URL unchanged\n *\n * @module @quilltap/plugin-utils/host-rewrite\n */\n\nimport { createPluginLogger } from './logging';\nimport { existsSync, statSync } from 'node:fs';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** Hostnames that refer to the local loopback */\nconst LOCALHOST_HOSTS = new Set([\n 'localhost',\n '127.0.0.1',\n '[::1]',\n '::1',\n]);\n\n// ============================================================================\n// Cached Gateway Host\n// ============================================================================\n\nlet cachedGatewayHost: string | null | undefined; // undefined = not yet resolved\n\nconst rewriteLogger = createPluginLogger('host-rewrite');\n\n// ============================================================================\n// Environment Detection (self-contained)\n// ============================================================================\n\n/**\n * Check if running in a Docker container.\n *\n * Detects Docker by checking:\n * 1. DOCKER_CONTAINER environment variable\n * 2. Existence of /.dockerenv file\n * 3. Existence of /app directory (Quilltap Docker convention)\n */\nfunction isDockerEnvironment(): boolean {\n if (process.env.DOCKER_CONTAINER === 'true') {\n return true;\n }\n\n // Check for Docker-specific markers\n try {\n if (existsSync('/.dockerenv')) {\n return true;\n }\n // Check for /app as a directory (Quilltap Docker convention)\n const appStat = statSync('/app');\n if (appStat.isDirectory()) {\n return true;\n }\n } catch {\n // Not in Docker\n }\n\n return false;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Check if running in an environment that needs URL rewriting.\n *\n * Docker is detected automatically. A hand-rolled VM cannot be, so setting\n * `QUILLTAP_HOST_IP` is what opts one in — it both enables rewriting and\n * supplies the gateway address.\n */\nexport function isVMEnvironment(): boolean {\n return isDockerEnvironment() || !!process.env.QUILLTAP_HOST_IP;\n}\n\n/**\n * Resolve the host gateway address (IP or hostname).\n *\n * Tries multiple strategies in order; caches the result so file reads\n * only happen once per process lifetime.\n */\nexport function resolveHostGateway(): string | null {\n // Return cached result if already resolved\n if (cachedGatewayHost !== undefined) {\n return cachedGatewayHost;\n }\n\n // Strategy 1: Explicit env var override\n const envIP = process.env.QUILLTAP_HOST_IP;\n if (envIP) {\n rewriteLogger.info('Host gateway from QUILLTAP_HOST_IP', { host: envIP });\n cachedGatewayHost = envIP;\n return cachedGatewayHost;\n }\n\n // Strategy 2: Docker — use host.docker.internal directly as a hostname\n // Docker Desktop (macOS/Windows) provides built-in DNS resolution for\n // host.docker.internal via its DNS server (127.0.0.11). Linux Docker\n // needs --add-host=host.docker.internal:host-gateway (handled by the\n // start scripts). Either way, host.docker.internal correctly forwards\n // to services bound to the host's loopback (127.0.0.1).\n //\n // The bridge gateway IP (e.g. 172.17.0.1) is deliberately not used as a\n // fallback: it is only the Docker bridge interface, and services listening\n // on the host's localhost are NOT reachable through it.\n if (isDockerEnvironment()) {\n rewriteLogger.info('Docker environment detected — using host.docker.internal as gateway hostname');\n cachedGatewayHost = 'host.docker.internal';\n return cachedGatewayHost;\n }\n\n rewriteLogger.warn('Could not resolve host gateway — localhost URLs will not be rewritten');\n cachedGatewayHost = null;\n return cachedGatewayHost;\n}\n\n/**\n * Rewrite a localhost URL to point at the host gateway.\n *\n * No-ops when:\n * - Not running in a container (and QUILLTAP_HOST_IP is unset)\n * - The URL doesn't point to localhost or 127.0.0.1\n * - Gateway resolution fails\n *\n * @param url The URL to potentially rewrite\n * @returns The original URL or a rewritten version with the gateway host\n */\nexport function rewriteLocalhostUrl(url: string): string {\n // No-op on bare metal\n if (!isVMEnvironment()) {\n return url;\n }\n\n // Parse the URL to check the hostname\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n // Not a valid URL — return unchanged\n return url;\n }\n\n // Check if hostname is a localhost variant\n if (!LOCALHOST_HOSTS.has(parsed.hostname)) {\n return url;\n }\n\n // Resolve the gateway host\n const gatewayHost = resolveHostGateway();\n if (!gatewayHost) {\n return url;\n }\n\n // Rewrite the hostname\n parsed.hostname = gatewayHost;\n const rewritten = parsed.toString();\n\n rewriteLogger.debug('Rewrote localhost URL', {\n original: url,\n rewritten,\n gatewayHost,\n });\n\n return rewritten;\n}\n\n/**\n * Reset the cached gateway host (for testing).\n * @internal\n */\nexport function _resetGatewayCache(): void {\n cachedGatewayHost = undefined;\n}\n","/**\n * GENERATED FILE — DO NOT EDIT.\n *\n * Written by scripts/generate-version.mjs from the \"version\" field in\n * package.json. Bump the version there; this file follows automatically on\n * `npm run build` / `npm run typecheck`.\n */\n\n/**\n * Version of the plugin-utils package.\n * Can be used at runtime to check compatibility.\n */\nexport const PLUGIN_UTILS_VERSION = '2.7.0';\n"],"mappings":"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","names":["OpenAI"]}
|
package/dist/index.mjs
CHANGED
|
@@ -624,6 +624,34 @@ var OpenAICompatibleProvider = class {
|
|
|
624
624
|
this.applyProfileParams(body, params);
|
|
625
625
|
return body;
|
|
626
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* Normalizes a Chat Completions `usage` block into Quilltap's shape.
|
|
629
|
+
*
|
|
630
|
+
* OpenAI-compatible endpoints fold prompt-cache hits into `prompt_tokens`
|
|
631
|
+
* (reported separately as `prompt_tokens_details.cached_tokens`). Quilltap's
|
|
632
|
+
* convention is that `promptTokens` / `totalTokens` EXCLUDE cache reads, so
|
|
633
|
+
* cached input never counts against token budgets; `cacheUsage` still
|
|
634
|
+
* carries the cache reads for display. Endpoints that report no
|
|
635
|
+
* `cached_tokens` are unaffected.
|
|
636
|
+
*/
|
|
637
|
+
normalizeUsage(usage) {
|
|
638
|
+
const cachedTokens = usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
639
|
+
if (cachedTokens > 0) {
|
|
640
|
+
this.logger.debug(`${this.providerName} usage: excluding prompt-cache hits from prompt/total`, {
|
|
641
|
+
context: `${this.providerName}Provider.normalizeUsage`,
|
|
642
|
+
promptTokens: usage?.prompt_tokens ?? 0,
|
|
643
|
+
cachedTokens
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
usage: {
|
|
648
|
+
promptTokens: Math.max(0, (usage?.prompt_tokens ?? 0) - cachedTokens),
|
|
649
|
+
completionTokens: usage?.completion_tokens ?? 0,
|
|
650
|
+
totalTokens: Math.max(0, (usage?.total_tokens ?? 0) - cachedTokens)
|
|
651
|
+
},
|
|
652
|
+
...cachedTokens > 0 ? { cacheUsage: { cacheReadInputTokens: cachedTokens, cachedTokens } } : {}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
627
655
|
/**
|
|
628
656
|
* Sends a message and returns the complete response.
|
|
629
657
|
*
|
|
@@ -646,11 +674,7 @@ var OpenAICompatibleProvider = class {
|
|
|
646
674
|
return {
|
|
647
675
|
content: choice.message.content ?? "",
|
|
648
676
|
finishReason: choice.finish_reason,
|
|
649
|
-
usage
|
|
650
|
-
promptTokens: response.usage?.prompt_tokens ?? 0,
|
|
651
|
-
completionTokens: response.usage?.completion_tokens ?? 0,
|
|
652
|
-
totalTokens: response.usage?.total_tokens ?? 0
|
|
653
|
-
},
|
|
677
|
+
...this.normalizeUsage(response.usage),
|
|
654
678
|
raw: response,
|
|
655
679
|
attachmentResults,
|
|
656
680
|
...toolCalls.length > 0 ? { toolCalls } : {}
|
|
@@ -691,11 +715,7 @@ var OpenAICompatibleProvider = class {
|
|
|
691
715
|
const content = choice?.delta?.content;
|
|
692
716
|
const hasUsage = chunk.usage;
|
|
693
717
|
if (hasUsage) {
|
|
694
|
-
accumulatedUsage =
|
|
695
|
-
prompt_tokens: chunk.usage?.prompt_tokens,
|
|
696
|
-
completion_tokens: chunk.usage?.completion_tokens,
|
|
697
|
-
total_tokens: chunk.usage?.total_tokens
|
|
698
|
-
};
|
|
718
|
+
accumulatedUsage = chunk.usage ?? null;
|
|
699
719
|
}
|
|
700
720
|
if (choice?.delta?.tool_calls) {
|
|
701
721
|
for (const tcDelta of choice.delta.tool_calls) {
|
|
@@ -723,11 +743,10 @@ var OpenAICompatibleProvider = class {
|
|
|
723
743
|
yield {
|
|
724
744
|
content: "",
|
|
725
745
|
done: true,
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
} : void 0,
|
|
746
|
+
...accumulatedUsage ? {
|
|
747
|
+
...this.normalizeUsage(accumulatedUsage),
|
|
748
|
+
rawProviderUsage: accumulatedUsage
|
|
749
|
+
} : {},
|
|
731
750
|
attachmentResults,
|
|
732
751
|
// Only synthesized when the stream actually carried tool calls: the
|
|
733
752
|
// host's tool detection reads `rawResponse`, and an empty one on every
|
|
@@ -987,7 +1006,7 @@ function rewriteLocalhostUrl(url) {
|
|
|
987
1006
|
}
|
|
988
1007
|
|
|
989
1008
|
// src/version.generated.ts
|
|
990
|
-
var PLUGIN_UTILS_VERSION = "2.
|
|
1009
|
+
var PLUGIN_UTILS_VERSION = "2.7.0";
|
|
991
1010
|
export {
|
|
992
1011
|
BUILTIN_TOOL_NAMES,
|
|
993
1012
|
DEFAULT_REQUEST_TIMEOUT_MS,
|