import OpenAI from 'openai' import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base' import { withBedrockDefaults } from '../utils/client' import type { Modality } from '@tanstack/ai' import type { BedrockClientConfig } from '../utils/client' import type { BedrockMessageMetadataByModality } from '../message-types' import type { BedrockChatModelToolCapabilitiesByName, BedrockChatModels, ResolveInputModalities, ResolveProviderOptions, } from '../model-meta' export interface BedrockTextConfig extends BedrockClientConfig {} export type { ExternalTextProviderOptions as BedrockTextProviderOptions } from '../text/text-provider-options' type ResolveToolCapabilities = TModel extends keyof BedrockChatModelToolCapabilitiesByName ? NonNullable : readonly [] /** * Bedrock Chat Completions adapter. Drives Bedrock's OpenAI-compatible * `/chat/completions` endpoint via the OpenAI SDK with a baseURL override * (same pattern as ai-groq). Tool conversion, streaming, structured output, * and the agent loop come from the base. */ export class BedrockTextAdapter< TModel extends BedrockChatModels, // Constraint mirrors ai-groq and the base, which parameterises // `TProviderOptions extends Record`. Our default // `ResolveProviderOptions` resolves to the `BedrockTextProviderOptions` // interface, which (lacking an implicit index signature) `Record` would reject but `Record` accepts. This `any` is // confined to the generic constraint (the established ai-groq pattern) — no // value/shape `as` cast is introduced. TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, TToolCapabilities extends ReadonlyArray = ResolveToolCapabilities, > extends OpenAIBaseChatCompletionsTextAdapter< TModel, TProviderOptions, TInputModalities, BedrockMessageMetadataByModality, TToolCapabilities > { override readonly kind = 'text' as const override readonly name = 'bedrock' as const constructor(config: BedrockTextConfig, model: TModel) { // No `forced` -> honors config.endpoint ('runtime' default, 'mantle' allowed). super( model, 'bedrock', new OpenAI(withBedrockDefaults(config, undefined, model)), ) } /** * Surface reasoning deltas (gpt-oss / Claude reasoning) the OpenAI-compatible * way. Base types the chunk as `unknown`; narrow with runtime guards — no * `as` casts, no `any`. */ protected override extractReasoning( chunk: unknown, ): { text: string } | undefined { return readDeltaReasoning(chunk) } } /** Cast-free narrowing of a Chat Completions chunk's reasoning delta. */ function readDeltaReasoning(chunk: unknown): { text: string } | undefined { if (typeof chunk !== 'object' || chunk === null || !('choices' in chunk)) return undefined if (!Array.isArray(chunk.choices)) return undefined const choice: unknown = chunk.choices[0] if (typeof choice !== 'object' || choice === null || !('delta' in choice)) return undefined const delta = choice.delta if (typeof delta !== 'object' || delta === null) return undefined const raw = 'reasoning' in delta && typeof delta.reasoning === 'string' ? delta.reasoning : 'reasoning_content' in delta && typeof delta.reasoning_content === 'string' ? delta.reasoning_content : undefined return raw && raw.length > 0 ? { text: raw } : undefined } /** Chat adapter with an explicit API key (low-level; the public branching factory delegates here). */ export function createBedrockChat( model: TModel, apiKey: string, config?: Omit, ): BedrockTextAdapter { return new BedrockTextAdapter({ ...config, apiKey }, model) }