diff --git a/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts b/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts index 3e6bb0ac4..994a43fe8 100644 --- a/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts +++ b/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts @@ -1,8 +1,10 @@ import type { LanguageModelV2 } from "@ai-sdk/provider" import { type FetchFunction, withoutTrailingSlash } from "@ai-sdk/provider-utils" import { OpenAICompatibleChatLanguageModel } from "../copilot/chat/openai-compatible-chat-language-model" +import { Log } from "@/util/log" const VERSION = "1.0.0" +const log = Log.create({ service: "provider.toothfairyai" }) export type ToothFairyAIModelId = string @@ -44,6 +46,7 @@ export function createToothFairyAI(options: ToothFairyAIProviderSettings = {}): if (url.pathname === "/chat/completions") { url.pathname = "/predictions" + log.debug("redirecting /chat/completions to /predictions") } let body: Record | undefined @@ -63,6 +66,34 @@ export function createToothFairyAI(options: ToothFairyAIProviderSettings = {}): } headers.delete("Authorization") + log.info("request", { + url: url.href, + method: init?.method ?? "GET", + hasTools: !!body?.tools, + toolCount: (body?.tools as any[])?.length ?? 0, + model: body?.model, + }) + + log.debug("request body", { + messages: body?.messages + ? (body.messages as any[]).map((m) => ({ + role: m.role, + content: typeof m.content === "string" ? m.content.slice(0, 100) : m.content, + tool_calls: m.tool_calls, + tool_call_id: m.tool_call_id, + })) + : undefined, + tools: body?.tools + ? (body.tools as any[]).map((t) => ({ + name: t.function?.name, + description: t.function?.description?.slice(0, 50), + })) + : undefined, + tool_choice: body?.tool_choice, + max_tokens: body?.max_tokens, + temperature: body?.temperature, + }) + const fetchFn = options.fetch ?? globalThis.fetch const res = await fetchFn(url, { ...init, @@ -70,6 +101,21 @@ export function createToothFairyAI(options: ToothFairyAIProviderSettings = {}): body: body ? JSON.stringify(body) : init?.body, }) + log.info("response", { + status: res.status, + statusText: res.statusText, + contentType: res.headers.get("content-type"), + }) + + if (!res.ok) { + const errorBody = await res.text().catch(() => "Unable to read error body") + log.error("request failed", { + status: res.status, + statusText: res.statusText, + errorBody: errorBody.slice(0, 500), + }) + } + if (res.body && res.headers.get("content-type")?.includes("text/event-stream")) { const reader = res.body.getReader() const decoder = new TextDecoder() @@ -92,6 +138,20 @@ export function createToothFairyAI(options: ToothFairyAIProviderSettings = {}): const json = line.slice(6).trim() if (json && !json.startsWith('{"status":')) { filtered.push(line) + // Log tool calls and finish_reason + try { + const parsed = JSON.parse(json) + if (parsed.choices?.[0]?.delta?.tool_calls) { + log.debug("stream tool_calls", { + tool_calls: parsed.choices[0].delta.tool_calls, + }) + } + if (parsed.choices?.[0]?.finish_reason) { + log.info("stream finish_reason", { + finish_reason: parsed.choices[0].finish_reason, + }) + } + } catch {} } } else { filtered.push(line)