Skip to content

Commit 9c3cbf8

Browse files
committed
feat: add detailed debugging and troubleshooting guidance for Gemini API; enhance translation handling for nested function responses
1 parent 57f7fd3 commit 9c3cbf8

3 files changed

Lines changed: 199 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Development Commands
6+
7+
- **Build**: `bun run build` (uses tsdown)
8+
- **Development**: `bun run dev` (with file watching)
9+
- **Production**: `bun run start` (sets NODE_ENV=production)
10+
- **Lint**: `bun run lint` (uses @echristian/eslint-config with cache)
11+
- **Lint fix**: `bunx lint-staged` (fixes staged files)
12+
- **Typecheck**: `bun run typecheck` (runs TypeScript compiler)
13+
- **Test all**: `bun test`
14+
- **Test single file**: `bun test tests/[filename].test.ts`
15+
- **Package**: `bun run prepack` (builds before packaging)
16+
17+
## Project Architecture
18+
19+
### High-Level Structure
20+
This is a GitHub Copilot API proxy server that exposes Copilot as OpenAI-compatible, Anthropic-compatible, and Gemini-compatible APIs. The server is built with Hono framework and uses Bun as the runtime.
21+
22+
### Core Architecture Components
23+
24+
**API Translation Layer** (`src/routes/messages/`):
25+
- Translates between Anthropic Messages API format and OpenAI Chat Completions format
26+
- Translates between Gemini API format and OpenAI Chat Completions format
27+
- Handles both streaming and non-streaming responses
28+
- Key files: `handler.ts`, `anthropic-types.ts`, `stream-translation.ts`, `non-stream-translation.ts`
29+
- Gemini files: `gemini-handler.ts`, `gemini-translation.ts`, `gemini-types.ts`, `gemini-route.ts`
30+
31+
**Token Counting for Anthropic Models** (`src/lib/tokenizer.ts`):
32+
- Uses `gpt-tokenizer/model/gpt-4o` for token counting
33+
- Separates input tokens (all messages except last assistant message) from output tokens (last assistant message)
34+
- Filters out tool messages and extracts text content from multipart messages
35+
- Used by `/v1/messages/count_tokens` endpoint for Anthropic compatibility
36+
37+
**GitHub Copilot Integration** (`src/services/`):
38+
- Authentication flow using device code OAuth
39+
- Token management and refresh
40+
- API requests to GitHub Copilot endpoints
41+
- Usage monitoring and quota tracking
42+
43+
**Rate Limiting & Controls** (`src/lib/`):
44+
- Rate limiting between requests (`rate-limit.ts`)
45+
- Manual approval system for requests (`approval.ts`)
46+
- State management for server configuration (`state.ts`)
47+
48+
### API Endpoints Structure
49+
50+
**OpenAI Compatible**:
51+
- `/v1/chat/completions` - Chat completions
52+
- `/v1/models` - Available models
53+
- `/v1/embeddings` - Text embeddings
54+
55+
**Anthropic Compatible**:
56+
- `/v1/messages` - Message completions (translates to/from OpenAI format)
57+
- `/v1/messages/count_tokens` - Token counting for Anthropic format
58+
59+
**Gemini Compatible**:
60+
- `/v1beta/models/{model}:generateContent` - Standard generation
61+
- `/v1beta/models/{model}:streamGenerateContent` - Streaming generation
62+
- `/v1beta/models/{model}:countTokens` - Token counting
63+
64+
**Monitoring**:
65+
- `/usage` - GitHub Copilot usage dashboard
66+
- `/token` - Current Copilot token info
67+
68+
### Key Implementation Details
69+
70+
**Anthropic Token Counting**:
71+
The `getTokenCount()` function in `src/lib/tokenizer.ts` implements token counting specifically for Anthropic compatibility:
72+
- Converts multipart content to text-only for counting
73+
- Splits messages into input (all except last assistant) and output (last assistant message only)
74+
- Uses GPT-4o tokenizer as the underlying counting mechanism
75+
- Returns `{input: number, output: number}` format
76+
77+
**Message Translation**:
78+
- OpenAI → Anthropic: Converts chat completion responses to Anthropic message format
79+
- Anthropic → OpenAI: Converts Anthropic message requests to OpenAI chat completion format
80+
- OpenAI → Gemini: Converts chat completion responses to Gemini response format
81+
- Gemini → OpenAI: Converts Gemini requests to OpenAI chat completion format
82+
- Handles tool calls, system messages, and content blocks appropriately for all formats
83+
84+
**Streaming Translation**:
85+
Real-time conversion of OpenAI SSE chunks to both Anthropic streaming events and Gemini streaming responses, maintaining state for proper message reconstruction.
86+
87+
**Gemini API Implementation**:
88+
The Gemini integration (`src/routes/messages/gemini-*`) provides:
89+
- Full compatibility with Google's Gemini API specification
90+
- Comprehensive request/response translation between Gemini and OpenAI formats
91+
- Support for function calling, multimodal content (text + images), and streaming
92+
- Extensive debug logging with file-based logs in `logs/` directory
93+
- Error handling with appropriate HTTP status codes and Gemini-formatted error responses
94+
- Support for generation configuration (temperature, max tokens, top-p, stop sequences)
95+
96+
**Critical Gemini Translation Details**:
97+
- Gemini CLI sends function responses as **nested arrays** in contents, requiring special handling
98+
- `parametersJsonSchema` field takes precedence over `parameters` in function declarations
99+
- Tool call ID mapping must be maintained between assistant tool calls and user tool responses
100+
- Function response arrays need extraction with `processFunctionResponseArray()` helper
101+
- Debug logs in `logs/gemini-*.log` files are essential for troubleshooting translation issues
102+
103+
## Code Style & Conventions
104+
105+
- **TypeScript**: Strict mode enabled, avoid `any` types
106+
- **Imports**: Use `~/*` path aliases for `src/*` imports
107+
- **Error Handling**: Use explicit error classes from `src/lib/error.ts`
108+
- **Testing**: Place tests in `tests/` directory with `*.test.ts` naming
109+
- **Formatting**: Prettier with package.json plugin
110+
- **Linting**: @echristian/eslint-config with strict rules
111+
112+
## Important Notes
113+
114+
- Server uses GitHub Copilot as the underlying LLM provider
115+
- Rate limiting and manual approval features help avoid GitHub abuse detection
116+
- Token counting uses GPT-4o tokenizer regardless of the actual model being proxied
117+
- All API translations maintain compatibility with OpenAI, Anthropic, and Gemini client libraries
118+
- Gemini API debugging logs are written to `logs/` directory for troubleshooting translation issues
119+
120+
## Debugging & Troubleshooting
121+
122+
**Common Gemini API Issues**:
123+
- **Function calls fail while text prompts work**: Check `logs/gemini-translation.log` for missing `parameters` in translated tools
124+
- **Tool response mapping errors**: Verify tool_call_id consistency between assistant tool calls and user tool responses
125+
- **Nested array handling**: Gemini CLI sends function responses as nested arrays requiring `processFunctionResponseArray()` extraction
126+
- **HTTPError from create-chat-completions**: Usually indicates parameter validation failure in OpenAI translation layer
127+
128+
**Key Log Files**:
129+
- `logs/gemini-errors.log`: HTTP errors and stack traces
130+
- `logs/gemini-debug.log`: Request/response flow with full JSON payloads
131+
- `logs/gemini-translation.log`: Translation pipeline details showing input/output transformations
132+
133+
**Debugging Commands**:
134+
- `bun run lint && bun run typecheck && bun run build`: Full validation pipeline
135+
- Check error reports in `C:\Users\39764\AppData\Local\Temp\gemini-client-error-*.json` for client-side failures

src/routes/messages/gemini-translation.ts

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,41 @@ export function translateGeminiToOpenAIStream(
6666
return result
6767
}
6868

69+
// Helper function to process function response arrays
70+
function processFunctionResponseArray(
71+
responseArray: Array<{
72+
functionResponse: { name: string; response: unknown }
73+
}>,
74+
pendingToolCalls: Map<string, string>,
75+
messages: Array<Message>,
76+
): void {
77+
for (const responseItem of responseArray) {
78+
if ("functionResponse" in responseItem) {
79+
const functionName = responseItem.functionResponse.name
80+
const toolCallId = pendingToolCalls.get(functionName)
81+
if (toolCallId) {
82+
messages.push({
83+
role: "tool",
84+
tool_call_id: toolCallId,
85+
content: JSON.stringify(responseItem.functionResponse.response),
86+
})
87+
pendingToolCalls.delete(functionName)
88+
}
89+
}
90+
}
91+
}
92+
6993
function translateGeminiContentsToOpenAI(
70-
contents: Array<GeminiContent>,
94+
contents: Array<
95+
| GeminiContent
96+
| Array<{
97+
functionResponse: { id?: string; name: string; response: unknown }
98+
}>
99+
>,
71100
systemInstruction?: GeminiContent,
72101
): Array<Message> {
73102
const messages: Array<Message> = []
103+
const pendingToolCalls = new Map<string, string>() // function name -> tool_call_id
74104

75105
// Add system instruction first if present
76106
if (systemInstruction) {
@@ -81,7 +111,14 @@ function translateGeminiContentsToOpenAI(
81111
}
82112

83113
// Process conversation contents
84-
for (const content of contents) {
114+
for (const item of contents) {
115+
// Handle special case where Gemini CLI sends function responses as nested arrays
116+
if (Array.isArray(item)) {
117+
processFunctionResponseArray(item, pendingToolCalls, messages)
118+
continue
119+
}
120+
121+
const content = item
85122
const role = content.role === "model" ? "assistant" : "user"
86123

87124
// Check for function calls/responses
@@ -95,28 +132,39 @@ function translateGeminiContentsToOpenAI(
95132
if (functionResponses.length > 0) {
96133
// Add tool result messages
97134
for (const funcResponse of functionResponses) {
98-
messages.push({
99-
role: "tool",
100-
tool_call_id: generateToolCallId(funcResponse.functionResponse.name),
101-
content: JSON.stringify(funcResponse.functionResponse.response),
102-
})
135+
const functionName = funcResponse.functionResponse.name
136+
const toolCallId = pendingToolCalls.get(functionName)
137+
if (toolCallId) {
138+
messages.push({
139+
role: "tool",
140+
tool_call_id: toolCallId,
141+
content: JSON.stringify(funcResponse.functionResponse.response),
142+
})
143+
pendingToolCalls.delete(functionName)
144+
}
103145
}
104146
}
105147

106148
if (functionCalls.length > 0 && role === "assistant") {
107149
// Assistant message with tool calls
108150
const textContent = extractTextFromGeminiContent(content)
109-
messages.push({
110-
role: "assistant",
111-
content: textContent || null,
112-
tool_calls: functionCalls.map((call) => ({
113-
id: generateToolCallId(call.functionCall.name),
114-
type: "function",
151+
const toolCalls = functionCalls.map((call) => {
152+
const toolCallId = generateToolCallId(call.functionCall.name)
153+
// Remember this tool call for later matching with responses
154+
pendingToolCalls.set(call.functionCall.name, toolCallId)
155+
return {
156+
id: toolCallId,
157+
type: "function" as const,
115158
function: {
116159
name: call.functionCall.name,
117160
arguments: JSON.stringify(call.functionCall.args),
118161
},
119-
})),
162+
}
163+
})
164+
messages.push({
165+
role: "assistant",
166+
content: textContent || null,
167+
tool_calls: toolCalls,
120168
})
121169
} else {
122170
// Regular message
@@ -180,7 +228,7 @@ function translateGeminiToolsToOpenAI(
180228
function: {
181229
name: func.name,
182230
description: func.description,
183-
parameters: func.parameters,
231+
parameters: func.parametersJsonSchema || func.parameters,
184232
},
185233
})
186234
}

src/routes/messages/gemini-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface GeminiFunctionDeclaration {
5353
name: string
5454
description?: string
5555
parameters: Record<string, unknown>
56+
parametersJsonSchema?: Record<string, unknown>
5657
}
5758

5859
export interface GeminiToolConfig {

0 commit comments

Comments
 (0)