import { Effect } from "effect"; import { tryPromise } from "./effect-runtime"; import { readOpenAIResponseStreamEffect, requestOpenAIResponseEffect, } from "./openai-response-runtime"; import { defaultRuntimeServices, type RuntimeServices, } from "./runtime-services"; const DEFAULT_MODEL = "gpt-5.5"; const DEFAULT_REASONING_EFFORT = "medium"; const DEFAULT_SERVICE_TIER = "priority"; const DEFAULT_DELIMITER_PATTERN = /\n---\s*\n/; export type AnalysisReasoningEffort = "minimal" | "low" | "medium" | "high"; export type AnalysisServiceTier = "default" | "flex" | "priority"; export interface AnalysisModelOptions { model?: string; reasoningEffort?: AnalysisReasoningEffort; serviceTier?: AnalysisServiceTier; } export interface AnalysisModelSettings { model: string; reasoningEffort: AnalysisReasoningEffort; serviceTier: AnalysisServiceTier; } export interface HybridAnalysisResult { value: T; markdown: string; rawText: string; responseId?: string; usage?: unknown; } function toError(error: unknown) { return error instanceof Error ? error : new Error(String(error)); } export function resolveAnalysisModelSettings( options: AnalysisModelOptions, runtime: RuntimeServices = defaultRuntimeServices, ): AnalysisModelSettings { return { model: options.model ?? runtime.env("BIRDCLAW_AI_MODEL") ?? DEFAULT_MODEL, reasoningEffort: options.reasoningEffort ?? (runtime.env( "BIRDCLAW_OPENAI_REASONING_EFFORT", ) as AnalysisReasoningEffort) ?? DEFAULT_REASONING_EFFORT, serviceTier: options.serviceTier ?? (runtime.env("BIRDCLAW_OPENAI_SERVICE_TIER") as AnalysisServiceTier) ?? DEFAULT_SERVICE_TIER, }; } export function createAnalysisRequestBody({ settings, system, prompt, stream, maxOutputTokens = 7000, }: { settings: AnalysisModelSettings; system: string; prompt: string; stream: boolean; maxOutputTokens?: number; }) { return { model: settings.model, reasoning: { effort: settings.reasoningEffort }, service_tier: settings.serviceTier, store: false, ...(stream ? { stream: true } : {}), max_output_tokens: maxOutputTokens, input: [ { role: "system", content: system }, { role: "user", content: prompt }, ], }; } export function parseHybridAnalysis({ rawText, parse, fallback, delimiterPattern = DEFAULT_DELIMITER_PATTERN, }: { rawText: string; parse: (value: unknown) => T; fallback: (markdown: string) => T; delimiterPattern?: RegExp; }) { const [markdownPart, jsonPart] = rawText.split(delimiterPattern); const markdown = (markdownPart ?? rawText).trim(); const candidate = jsonPart?.slice( jsonPart.indexOf("{"), jsonPart.lastIndexOf("}") + 1, ); if (candidate?.startsWith("{")) { try { return { markdown, value: parse(JSON.parse(candidate)) }; } catch { return { markdown, value: fallback(markdown) }; } } return { markdown, value: fallback(markdown) }; } export function extractOpenAIResponseText(payload: Record) { if (typeof payload.output_text === "string") { return payload.output_text; } const output = Array.isArray(payload.output) ? payload.output : []; const parts: string[] = []; for (const item of output) { if (!item || typeof item !== "object") continue; const content = (item as { content?: unknown }).content; if (!Array.isArray(content)) continue; for (const block of content) { if (!block || typeof block !== "object") continue; const record = block as Record; if (typeof record.text === "string") parts.push(record.text); } } return parts.join(""); } export function readHybridAnalysisStreamEffect( response: Response, { parse, fallback, onDelta, delimiterPattern = DEFAULT_DELIMITER_PATTERN, }: { parse: (value: unknown) => T; fallback: (markdown: string) => T; onDelta?: (delta: string) => void; delimiterPattern?: RegExp; }, ): Effect.Effect, Error> { return Effect.gen(function* () { const stream = yield* readOpenAIResponseStreamEffect(response, { delimiterPattern, onDelta, }); const parsed = parseHybridAnalysis({ rawText: stream.rawText, parse, fallback, delimiterPattern, }); return { ...parsed, rawText: stream.rawText, ...(stream.responseId ? { responseId: stream.responseId } : {}), ...(stream.usage === undefined ? {} : { usage: stream.usage }), }; }); } export function streamHybridAnalysisEffect({ body, signal, runtime = defaultRuntimeServices, parse, fallback, onDelta, delimiterPattern = DEFAULT_DELIMITER_PATTERN, }: { body: unknown; signal?: AbortSignal; runtime?: RuntimeServices; parse: (value: unknown) => T; fallback: (markdown: string) => T; onDelta?: (delta: string) => void; delimiterPattern?: RegExp; }): Effect.Effect, Error> { return Effect.gen(function* () { const response = yield* requestOpenAIResponseEffect({ body, signal, runtime, }); return yield* readHybridAnalysisStreamEffect(response, { parse, fallback, onDelta, delimiterPattern, }); }); } export function requestHybridAnalysisEffect({ body, signal, runtime = defaultRuntimeServices, parse, fallback, delimiterPattern = DEFAULT_DELIMITER_PATTERN, }: { body: unknown; signal?: AbortSignal; runtime?: RuntimeServices; parse: (value: unknown) => T; fallback: (markdown: string) => T; delimiterPattern?: RegExp; }): Effect.Effect, Error> { return Effect.gen(function* () { const response = yield* requestOpenAIResponseEffect({ body, signal, runtime, }); const payload = (yield* tryPromise(() => response.json()).pipe( Effect.mapError(toError), )) as Record; const rawText = extractOpenAIResponseText(payload); if (!rawText) { return yield* Effect.fail(new Error("OpenAI returned no output text")); } const parsed = parseHybridAnalysis({ rawText, parse, fallback, delimiterPattern, }); return { ...parsed, rawText }; }); }