fix: normalize OpenAI/GitHub finish_reason to Flynn stopReason conventions
OpenAI-compatible providers return 'stop' and 'tool_calls' as finish_reason values, but Flynn's agent loop expects Anthropic-style 'end_turn' and 'tool_use'. This caused the agent to exit the tool loop prematurely when falling back to GitHub Copilot (due to Anthropic API quota exhaustion). - openai.ts: Map 'stop' → 'end_turn', 'length' → 'max_tokens', tool_calls with actual tools → 'tool_use', tool_calls without tools → 'end_turn' - github.ts: Handle edge case where finish_reason is 'tool_calls' but no tools were parsed - agent.ts: Accept both 'tool_use' and 'tool_calls' as valid stop reasons (belt-and-suspenders), extract toolCalls to local variable for TS narrowing - openai.test.ts: Update expectations to match new normalized values
This commit is contained in:
@@ -161,6 +161,9 @@ export class GitHubModelsClient implements ModelClient {
|
||||
stopReason = 'end_turn';
|
||||
} else if (reason === 'length') {
|
||||
stopReason = 'max_tokens';
|
||||
} else if (reason === 'tool_calls') {
|
||||
// Edge case: finish_reason says tool_calls but none were parsed
|
||||
stopReason = 'end_turn';
|
||||
} else {
|
||||
stopReason = reason ?? 'end_turn';
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('OpenAIClient', () => {
|
||||
});
|
||||
|
||||
expect(response.content).toBe('Hello from GPT!');
|
||||
expect(response.stopReason).toBe('stop');
|
||||
expect(response.stopReason).toBe('end_turn');
|
||||
expect(response.usage.inputTokens).toBe(10);
|
||||
expect(response.usage.outputTokens).toBe(5);
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe('OpenAIClient tool use', () => {
|
||||
}],
|
||||
});
|
||||
|
||||
expect(response.stopReason).toBe('tool_calls');
|
||||
expect(response.stopReason).toBe('tool_use');
|
||||
expect(response.toolCalls).toHaveLength(1);
|
||||
expect(response.toolCalls![0]).toEqual({
|
||||
id: 'call_1',
|
||||
|
||||
+19
-1
@@ -96,9 +96,27 @@ export class OpenAIClient implements ModelClient {
|
||||
args: JSON.parse(tc.function.arguments),
|
||||
})) ?? [];
|
||||
|
||||
// Map OpenAI finish reasons to Flynn's stop reasons
|
||||
let stopReason: string;
|
||||
if (toolCalls.length > 0) {
|
||||
stopReason = 'tool_use';
|
||||
} else {
|
||||
const reason = choice?.finish_reason;
|
||||
if (reason === 'stop') {
|
||||
stopReason = 'end_turn';
|
||||
} else if (reason === 'length') {
|
||||
stopReason = 'max_tokens';
|
||||
} else if (reason === 'tool_calls') {
|
||||
// Edge case: finish_reason says tool_calls but none were parsed
|
||||
stopReason = 'end_turn';
|
||||
} else {
|
||||
stopReason = reason ?? 'end_turn';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
stopReason: choice?.finish_reason ?? 'stop',
|
||||
stopReason,
|
||||
usage: {
|
||||
inputTokens: response.usage?.prompt_tokens ?? 0,
|
||||
outputTokens: response.usage?.completion_tokens ?? 0,
|
||||
|
||||
Reference in New Issue
Block a user