* feat(api): classify openai-compatible provider failures * Update src/services/api/providerConfig.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/services/api/errors.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(api): harden openai-compatible diagnostics and env fallback * Update src/services/api/openaiShim.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/services/api/openaiShim.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/services/api/errors.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/services/api/errors.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix openaiShim duplicate requests and diagnostics * remove unused url from http failure classifier * dedupe env diagnostic warnings * Remove hardcoded URLs from OpenAI error tests Removed hardcoded URLs from network failure classification tests. * Update providerConfig.envDiagnostics.test.ts * fix(openai-shim): return successful responses and restore localhost classifier tests * Update src/services/api/openaiShim.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/services/api/openaiShim.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/services/api/openaiShim.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import { expect, test } from 'bun:test'
|
|
|
|
import {
|
|
buildOpenAICompatibilityErrorMessage,
|
|
classifyOpenAIHttpFailure,
|
|
classifyOpenAINetworkFailure,
|
|
extractOpenAICategoryMarker,
|
|
formatOpenAICategoryMarker,
|
|
} from './openaiErrorClassification.js'
|
|
|
|
test('classifies localhost ECONNREFUSED as connection_refused', () => {
|
|
const error = Object.assign(new TypeError('fetch failed'), {
|
|
code: 'ECONNREFUSED',
|
|
})
|
|
|
|
const failure = classifyOpenAINetworkFailure(error, {
|
|
url: 'http://localhost:11434/v1/chat/completions',
|
|
})
|
|
|
|
expect(failure.category).toBe('connection_refused')
|
|
expect(failure.retryable).toBe(true)
|
|
expect(failure.code).toBe('ECONNREFUSED')
|
|
expect(failure.hint).toContain('local server is running')
|
|
})
|
|
|
|
test('classifies localhost ENOTFOUND as localhost_resolution_failed', () => {
|
|
const error = Object.assign(new TypeError('getaddrinfo ENOTFOUND localhost'), {
|
|
code: 'ENOTFOUND',
|
|
})
|
|
|
|
const failure = classifyOpenAINetworkFailure(error, {
|
|
url: 'http://localhost:11434/v1/chat/completions',
|
|
})
|
|
|
|
expect(failure.category).toBe('localhost_resolution_failed')
|
|
expect(failure.retryable).toBe(true)
|
|
expect(failure.code).toBe('ENOTFOUND')
|
|
expect(failure.hint).toContain('127.0.0.1')
|
|
})
|
|
|
|
test('classifies model-not-found 404 responses', () => {
|
|
const failure = classifyOpenAIHttpFailure({
|
|
status: 404,
|
|
body: 'The model qwen2.5-coder:7b was not found',
|
|
})
|
|
|
|
expect(failure.category).toBe('model_not_found')
|
|
expect(failure.retryable).toBe(false)
|
|
})
|
|
|
|
test('classifies generic 404 responses as endpoint_not_found', () => {
|
|
const failure = classifyOpenAIHttpFailure({
|
|
status: 404,
|
|
body: 'Not Found',
|
|
})
|
|
|
|
expect(failure.category).toBe('endpoint_not_found')
|
|
expect(failure.hint).toContain('/v1')
|
|
})
|
|
|
|
test('classifies context-overflow responses', () => {
|
|
const failure = classifyOpenAIHttpFailure({
|
|
status: 500,
|
|
body: 'request too large: maximum context length exceeded',
|
|
})
|
|
|
|
expect(failure.category).toBe('context_overflow')
|
|
expect(failure.retryable).toBe(false)
|
|
})
|
|
|
|
test('classifies tool compatibility failures', () => {
|
|
const failure = classifyOpenAIHttpFailure({
|
|
status: 400,
|
|
body: 'tool_calls are not supported by this model',
|
|
})
|
|
|
|
expect(failure.category).toBe('tool_call_incompatible')
|
|
})
|
|
|
|
test('embeds and extracts category markers in formatted messages', () => {
|
|
const marker = formatOpenAICategoryMarker('endpoint_not_found')
|
|
expect(marker).toBe('[openai_category=endpoint_not_found]')
|
|
|
|
const formatted = buildOpenAICompatibilityErrorMessage('OpenAI API error 404: Not Found', {
|
|
category: 'endpoint_not_found',
|
|
hint: 'Confirm OPENAI_BASE_URL includes /v1.',
|
|
})
|
|
|
|
expect(formatted).toContain('[openai_category=endpoint_not_found]')
|
|
expect(formatted).toContain('Hint: Confirm OPENAI_BASE_URL includes /v1.')
|
|
expect(extractOpenAICategoryMarker(formatted)).toBe('endpoint_not_found')
|
|
})
|
|
|
|
test('ignores unknown category markers during extraction', () => {
|
|
const malformed = 'OpenAI API error 500 [openai_category=totally_fake_category]'
|
|
expect(extractOpenAICategoryMarker(malformed)).toBeUndefined()
|
|
})
|