← opencode report

T22 — Provider-specific message transformation

Centralize quirk của từng provider vào transform.ts: Anthropic split tool_use khỏi text, Mistral giới hạn toolCallId 9 chars, Gemini chỉ cho alphanumeric tool name.
Nhóm: D — Provider AbstractionFile: provider/transform.ts · Lines 48–150ID: D.2 / T22Status: Stable

Tổng quan Provider

Tại sao quan trọng. Mỗi LLM provider có quirks riêng trong format request/response mà không được document rõ hoặc thay đổi theo version. Nếu scatter quirk handling vào core logic → conditional spaghetti khó maintain. Centralize vào transform.ts — 1 file, 1 function per provider quirk → core logic clean; thêm provider mới chỉ cần thêm transform function, không touch core.
Quirks thực tế (production-discovered): Những quirks này được tìm ra qua production debugging, không phải documentation. Mỗi quirk trong transform.ts là "lesson learned" từ một lần provider API trả lỗi kỳ lạ.

Phân tích code chi tiết Anatomy

3 quirk chính được document

provider/transform.ts — per-provider transforms

TS
{`
// ── Anthropic quirk: tool_use phải tách khỏi text content ──
// Nếu assistant message có cả text VÀ tool_use trong cùng content array
// → Anthropic API reject với lỗi không rõ ràng
function anthropicTransform(msgs: Message[]): AnthropicMessage[] {
  return msgs.flatMap((m) => {
    if (m.role !== "assistant") return [m as AnthropicMessage]

    const toolUses = m.content.filter((c) => c.type === "tool_use")
    const nonTool  = m.content.filter((c) => c.type !== "tool_use")

    // Split thành 2 messages riêng nếu có cả hai
    if (toolUses.length && nonTool.length) {
      return [
        { role: "assistant", content: nonTool  },
        { role: "assistant", content: toolUses },
      ]
    }
    return [m as AnthropicMessage]
  })
}

// ── Mistral quirk: toolCallId phải <= 9 chars, alphanumeric ──
function mistralTransform(msgs: Message[]) {
  return msgs.map((m) => ({
    ...m,
    content: m.content.map((c) => {
      if ("toolCallId" in c) {
        return { ...c, toolCallId: padToolCallId(c.toolCallId) }
        // padToolCallId: truncate to 9 + pad với zeros nếu ngắn hơn
      }
      return c
    }),
  }))
}

// ── Claude/Gemini quirk: tool name chỉ được có [a-zA-Z0-9_-] ──
function sanitizeToolName(name: string): string {
  return name.replace(/[^a-zA-Z0-9_-]/g, "_")
}
`}

Transform registry — 1 file mọi quirks

Transform dispatch

TS
{`
const TRANSFORMS: Record<Provider, Transform> = {
  anthropic: anthropicTransform,
  openai:    identity,          // OpenAI chuẩn nhất, ít quirks
  mistral:   mistralTransform,
  google:    googleTransform,   // move system message to start
  bedrock:   bedrockTransform,  // XML content formatting
  // ...
}

export function prepareMessages(provider: Provider, msgs: Message[]): Message[] {
  const transform = TRANSFORMS[provider] ?? identity
  return transform(msgs)
}

// Core call function — không biết gì về quirks
async function callModel(provider: Provider, messages: Msg[]) {
  const prepared = prepareMessages(provider, messages)  // ← quirks handled
  return getClient(provider).stream({ messages: prepared })
}
`}

Tương tác với kỹ thuật khác Interaction

  • T21 (Provider lazy loading): Transform được apply sau khi provider đã được loaded. prepareMessages(provider, messages) dùng provider ID để lookup transform.
  • T23 (Overflow + retry): Transform xảy ra trước khi gọi API — nếu transform fail (vd không sanitize được tool name), lỗi xảy ra trước API call, không cần retry.
  • T10 (Cache-aware prompt): Anthropic transform phải chú ý không phá vỡ cache breakpoint khi split messages — split sai vị trí = invalidate cache = cost tăng.

Failure modes Failure

1. Provider thay spec không thông báo

Provider update message format (vd Anthropic thêm restriction mới với tool_use), transform cũ không xử lý → API lỗi. Cần monitor API errors để detect transform mismatch sớm.

2. Transform làm mất fidelity

Truncate Mistral toolCallId từ "call_abc123xyz""call_abc1" (9 chars) → khó trace tool call trong logging/debugging. Cần map ngược toolCallId trong response processing.

Debug workflow: Khi debug provider-specific issue, luôn log messages trướcsau transform để thấy rõ quirk nào đã được applied.

So sánh với các harness khác Compare

HarnessQuirk handlingCentralization
opencodetransform.ts — 1 function per provider✅ highly centralized
Aiderlitellm xử lý quirks✅ outsourced to litellm
Claude CodeProvider-specific code sections⚠️ moderate
ClineInline conditional trong call path❌ scattered
OpenHarnessSDK-level handling (Anthropic/OpenAI SDK)⚠️ SDK-dependent

Implementation recipe Recipe

TS
{`
type Message = { role: string; content: ContentBlock[] }
type Transform = (msgs: Message[]) => Message[]
const identity: Transform = (msgs) => msgs

// Thêm quirk mới: chỉ cần thêm 1 function + register
function geminiTransform(msgs: Message[]): Message[] {
  // Gemini không cho system message sau user message
  const system = msgs.filter(m => m.role === "system")
  const rest   = msgs.filter(m => m.role !== "system")
  return [...system, ...rest]
}

const TRANSFORMS: Record<string, Transform> = {
  anthropic:  anthropicSplitToolUse,
  mistral:    mistralTruncateToolCallId,
  google:     geminiTransform,
  openai:     identity,
  // Thêm provider mới = thêm 1 dòng
}

export function applyProviderTransform(provider: string, msgs: Message[]): Message[] {
  return (TRANSFORMS[provider] ?? identity)(msgs)
}
`}

Tham khảo Refs