Files
oh-my-openagent/src/plugin/chat-headers.ts
Zhiyuan Zheng 890a737d1e fix(chat-headers): skip x-initiator override for @ai-sdk/github-copilot models
OpenCode's copilot fetch wrapper already sets x-initiator based on the
actual HTTP request body content. When oh-my-opencode's chat.headers
hook overrides it with 'agent', the Copilot API detects a mismatch
between the header and the request body and rejects the request with
'invalid initiator'.

This matches the approach OpenCode's own chat.headers handler uses
(copilot.ts:314) — it explicitly skips @ai-sdk/github-copilot models
because the fetch wrapper handles x-initiator correctly on its own.
2026-02-26 12:38:05 +08:00

142 lines
4.1 KiB
TypeScript

import { OMO_INTERNAL_INITIATOR_MARKER } from "../shared"
import type { PluginContext } from "./types"
type ChatHeadersInput = {
sessionID: string
provider: { id: string }
message: {
id?: string
role?: string
}
}
type ChatHeadersOutput = {
headers: Record<string, string>
}
const INTERNAL_MARKER_CACHE_LIMIT = 1000
const internalMarkerCache = new Map<string, boolean>()
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
function buildChatHeadersInput(raw: unknown): ChatHeadersInput | null {
if (!isRecord(raw)) return null
const sessionID = raw.sessionID
const provider = raw.provider
const message = raw.message
if (typeof sessionID !== "string") return null
if (!isRecord(provider) || typeof provider.id !== "string") return null
if (!isRecord(message)) return null
return {
sessionID,
provider: { id: provider.id },
message: {
id: typeof message.id === "string" ? message.id : undefined,
role: typeof message.role === "string" ? message.role : undefined,
},
}
}
function isChatHeadersOutput(raw: unknown): raw is ChatHeadersOutput {
if (!isRecord(raw)) return false
if (!isRecord(raw.headers)) {
raw.headers = {}
}
return isRecord(raw.headers)
}
function isCopilotProvider(providerID: string): boolean {
return providerID === "github-copilot" || providerID === "github-copilot-enterprise"
}
async function hasInternalMarker(
client: PluginContext["client"],
sessionID: string,
messageID: string,
): Promise<boolean> {
const cacheKey = `${sessionID}:${messageID}`
const cached = internalMarkerCache.get(cacheKey)
if (cached !== undefined) {
return cached
}
try {
const response = await client.session.message({
path: { id: sessionID, messageID },
})
const data = response.data
if (!isRecord(data) || !Array.isArray(data.parts)) {
internalMarkerCache.set(cacheKey, false)
if (internalMarkerCache.size > INTERNAL_MARKER_CACHE_LIMIT) {
internalMarkerCache.clear()
}
return false
}
const hasMarker = data.parts.some((part) => {
if (!isRecord(part) || part.type !== "text" || typeof part.text !== "string") {
return false
}
return part.text.includes(OMO_INTERNAL_INITIATOR_MARKER)
})
internalMarkerCache.set(cacheKey, hasMarker)
if (internalMarkerCache.size > INTERNAL_MARKER_CACHE_LIMIT) {
internalMarkerCache.clear()
}
return hasMarker
} catch {
internalMarkerCache.set(cacheKey, false)
if (internalMarkerCache.size > INTERNAL_MARKER_CACHE_LIMIT) {
internalMarkerCache.clear()
}
return false
}
}
async function isOmoInternalMessage(input: ChatHeadersInput, client: PluginContext["client"]): Promise<boolean> {
if (input.message.role !== "user") {
return false
}
if (!input.message.id) {
return false
}
return hasInternalMarker(client, input.sessionID, input.message.id)
}
export function createChatHeadersHandler(args: { ctx: PluginContext }): (input: unknown, output: unknown) => Promise<void> {
const { ctx } = args
return async (input, output): Promise<void> => {
const normalizedInput = buildChatHeadersInput(input)
if (!normalizedInput) return
if (!isChatHeadersOutput(output)) return
if (!isCopilotProvider(normalizedInput.provider.id)) return
// Do not override x-initiator when @ai-sdk/github-copilot is active.
// OpenCode's copilot fetch wrapper already sets x-initiator based on
// the actual request body content. Overriding it here causes a mismatch
// that the Copilot API rejects with "invalid initiator".
const model = isRecord(input) && isRecord((input as Record<string, unknown>).model)
? (input as Record<string, unknown>).model as Record<string, unknown>
: undefined
const api = model && isRecord(model.api) ? model.api as Record<string, unknown> : undefined
if (api?.npm === "@ai-sdk/github-copilot") return
if (!(await isOmoInternalMessage(normalizedInput, ctx.client))) return
output.headers["x-initiator"] = "agent"
}
}