SPB Git
6.1 KB · 116 lines typescript
1/**2 * Provider adapters used by discovery + benchmarks. One function per provider:3 * streaming text call with optional tools / JSON schema / image / reasoning control.4 */5import { openai, anthropic, now, errInfo, type CallResult, RED_PNG_B64 } from "./lib.ts";67export interface CallOpts {8  model: string;9  system?: string;10  prompt: string;11  maxTokens?: number;12  jsonSchema?: { name: string; schema: Record<string, unknown> };13  tools?: { name: string; description: string; parameters: Record<string, unknown> }[];14  image?: boolean;15  reasoning?: "low" | "medium" | "high" | "none";16  stream?: boolean;17  timeoutMs?: number;18}1920const isReasoningOpenAI = (m: string) => /^(o\d|gpt-5|gpt-6)/.test(m) && !/chat-latest|^chat-latest/.test(m);2122export async function callOpenAI(o: CallOpts): Promise<CallResult> {23  const t0 = now();24  let ttft: number | null = null;25  const content: any[] = [{ type: "input_text", text: o.prompt }];26  if (o.image) content.unshift({ type: "input_image", image_url: `data:image/png;base64,${RED_PNG_B64}`, detail: "low" });27  const body: any = {28    model: o.model,29    input: [{ role: "user", content }],30    max_output_tokens: o.maxTokens ?? 2048,31  };32  if (o.system) body.instructions = o.system;33  if (o.jsonSchema) body.text = { format: { type: "json_schema", name: o.jsonSchema.name, schema: o.jsonSchema.schema, strict: true } };34  if (o.tools) body.tools = o.tools.map((t) => ({ type: "function", name: t.name, description: t.description, parameters: t.parameters, strict: false }));35  if (o.reasoning && isReasoningOpenAI(o.model)) body.reasoning = { effort: o.reasoning };36  try {37    let text = "";38    let final: any;39    if (o.stream !== false) {40      const stream = await openai.responses.create({ ...body, stream: true }, { timeout: o.timeoutMs });41      for await (const ev of stream as any) {42        if (ev.type === "response.output_text.delta") { if (ttft === null) ttft = now() - t0; text += ev.delta; }43        else if (ev.type === "response.function_call_arguments.delta" && ttft === null) ttft = now() - t0;44        else if (ev.type === "response.completed") final = ev.response;45        else if (ev.type === "response.failed" || ev.type === "error") throw new Error(JSON.stringify(ev.response?.error ?? ev.error ?? ev).slice(0, 500));46        else if (ev.type === "response.incomplete") final = ev.response;47      }48    } else {49      final = await openai.responses.create(body, { timeout: o.timeoutMs });50      text = final.output_text ?? "";51      ttft = now() - t0;52    }53    const total = now() - t0;54    const toolCalls = (final?.output ?? []).filter((x: any) => x.type === "function_call").map((x: any) => ({ name: x.name, args: safeJSON(x.arguments) }));55    if (!text && final?.output_text) text = final.output_text;56    const u = final?.usage;57    return {58      ok: true, text, toolCalls, ttft_ms: ttft, total_ms: total, stop: final?.status ?? null,59      usage: u ? { input_tokens: u.input_tokens, output_tokens: u.output_tokens, cached_tokens: u.input_tokens_details?.cached_tokens ?? 0, reasoning_tokens: u.output_tokens_details?.reasoning_tokens ?? 0, source: "provider" } : null,60      raw: final?.incomplete_details ?? undefined,61    };62  } catch (e: any) {63    return { ok: false, text: "", toolCalls: [], usage: null, ttft_ms: ttft, total_ms: now() - t0, ...errInfo(e) };64  }65}6667export async function callAnthropic(o: CallOpts): Promise<CallResult> {68  const t0 = now();69  let ttft: number | null = null;70  const content: any[] = [{ type: "text", text: o.prompt }];71  if (o.image) content.unshift({ type: "image", source: { type: "base64", media_type: "image/png", data: RED_PNG_B64 } });72  const body: any = { model: o.model, max_tokens: o.maxTokens ?? 2048, messages: [{ role: "user", content }] };73  if (o.system) body.system = o.system;74  if (o.jsonSchema) body.output_config = { ...(body.output_config ?? {}), format: { type: "json_schema", schema: o.jsonSchema.schema } };75  if (o.tools) body.tools = o.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));76  if (o.reasoning && o.reasoning !== "none") {77    // Current models: adaptive thinking + effort. Older (4.5) models: budget_tokens.78    if (/4-5-\d{8}$/.test(o.model)) body.thinking = { type: "enabled", budget_tokens: 2048 }, body.max_tokens = Math.max(body.max_tokens, 4096);79    else { body.thinking = { type: "adaptive" }; body.output_config = { ...(body.output_config ?? {}), effort: o.reasoning }; }80  }81  try {82    let text = "";83    let final: any;84    if (o.stream !== false) {85      const stream = anthropic.messages.stream(body, { timeout: o.timeoutMs });86      for await (const ev of stream as any) {87        if (ev.type === "content_block_delta") {88          if (ev.delta.type === "text_delta") { if (ttft === null) ttft = now() - t0; text += ev.delta.text; }89          else if (ev.delta.type === "input_json_delta" && ttft === null) ttft = now() - t0;90        }91      }92      final = await stream.finalMessage();93    } else {94      final = await anthropic.messages.create(body, { timeout: o.timeoutMs });95      ttft = now() - t0;96      text = final.content.filter((b: any) => b.type === "text").map((b: any) => b.text).join("");97    }98    const total = now() - t0;99    const toolCalls = final.content.filter((b: any) => b.type === "tool_use").map((b: any) => ({ name: b.name, args: b.input }));100    const u = final.usage;101    return {102      ok: true, text, toolCalls, ttft_ms: ttft, total_ms: total, stop: final.stop_reason,103      usage: { input_tokens: u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0), output_tokens: u.output_tokens, cached_tokens: u.cache_read_input_tokens ?? 0, reasoning_tokens: u.output_tokens_details?.thinking_tokens ?? 0, source: "provider" },104      raw: final.stop_reason === "refusal" ? final.stop_details : undefined,105    };106  } catch (e: any) {107    return { ok: false, text: "", toolCalls: [], usage: null, ttft_ms: ttft, total_ms: now() - t0, ...errInfo(e) };108  }109}110111export function call(provider: "openai" | "anthropic", o: CallOpts) {112  return provider === "openai" ? callOpenAI(o) : callAnthropic(o);113}114115export function safeJSON(s: unknown) { try { return typeof s === "string" ? JSON.parse(s) : s; } catch { return s; } }116
Files · main
Menu

Theme