chore(lint): reduce warning debt across core adapters and model clients

This commit is contained in:
William Valentin
2026-02-15 23:03:42 -08:00
parent 92da407e22
commit 49b752e8b0
17 changed files with 239 additions and 117 deletions
+23 -9
View File
@@ -1,6 +1,6 @@
import Anthropic from '@anthropic-ai/sdk';
import type { Message as AnthropicMessage } from '@anthropic-ai/sdk/resources/messages/messages.js';
import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient, Message, MessageContentPart } from './types.js';
import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient, MessageContentPart } from './types.js';
export interface AnthropicClientConfig {
apiKey?: string; // Falls back to ANTHROPIC_API_KEY env var
@@ -23,21 +23,27 @@ function toAnthropicContent(content: string | MessageContentPart[]): string | un
}
if (part.type === 'image') {
if (part.source.type === 'base64') {
if (!part.source.data) {
return { type: 'text', text: '[Image omitted: missing base64 data]' };
}
return {
type: 'image',
source: {
type: 'base64',
media_type: part.source.media_type,
data: part.source.data!,
data: part.source.data,
},
};
}
if (!part.source.url) {
return { type: 'text', text: '[Image omitted: missing URL]' };
}
// URL-based image
return {
type: 'image',
source: {
type: 'url',
url: part.source.url!,
url: part.source.url,
},
};
}
@@ -52,6 +58,10 @@ function toAnthropicContent(content: string | MessageContentPart[]): string | un
});
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
}
export class AnthropicClient implements ModelClient {
private client: Anthropic;
private model: string;
@@ -67,14 +77,17 @@ export class AnthropicClient implements ModelClient {
}
async chat(request: ChatRequest): Promise<ChatResponse> {
const params: Record<string, unknown> = {
type CreateParams = Parameters<typeof this.client.messages.create>[0] & {
thinking?: { type: 'enabled'; budget_tokens: number };
};
const params: CreateParams = {
model: this.model,
max_tokens: request.maxTokens ?? this.defaultMaxTokens,
system: request.system,
messages: request.messages.map((m) => ({
role: m.role,
content: toAnthropicContent(m.content),
})),
})) as CreateParams['messages'],
};
if (request.tools && request.tools.length > 0) {
@@ -83,18 +96,19 @@ export class AnthropicClient implements ModelClient {
// Extended thinking mode — enable thinking with a budget
if (request.thinking) {
params.max_tokens = Math.max(params.max_tokens as number, 16384);
(params as any).thinking = { type: 'enabled', budget_tokens: 4096 };
params.max_tokens = Math.max(params.max_tokens, 16384);
params.thinking = { type: 'enabled', budget_tokens: 4096 };
}
const response = await this.client.messages.create(params as unknown as Parameters<typeof this.client.messages.create>[0]) as AnthropicMessage;
const response = await this.client.messages.create(params) as AnthropicMessage;
const textContent = response.content.find((c) => c.type === 'text');
const content = textContent?.type === 'text' ? textContent.text : '';
// Extract thinking content if present
const thinkingBlock = response.content.find((c) => c.type === 'thinking');
const thinkingContent = thinkingBlock && 'thinking' in thinkingBlock ? (thinkingBlock as any).text : undefined;
const thinkingText = asRecord(thinkingBlock)?.text;
const thinkingContent = typeof thinkingText === 'string' ? thinkingText : undefined;
const toolCalls = response.content
.filter((c): c is { type: 'tool_use'; id: string; name: string; input: unknown } => c.type === 'tool_use')
+12 -6
View File
@@ -137,8 +137,8 @@ export class OllamaClient implements ModelClient {
private async checkToolSupport(): Promise<boolean> {
if (this._supportsTools !== null) {return this._supportsTools;}
try {
const info = await this.client.show({ model: this.model });
const caps: string[] = (info as any).capabilities ?? [];
const info = await this.client.show({ model: this.model }) as { capabilities?: string[] };
const caps = info.capabilities ?? [];
this._supportsTools = caps.includes('tools');
} catch {
// Old Ollama or network issue — assume tools are supported
@@ -151,6 +151,12 @@ export class OllamaClient implements ModelClient {
* Convert Flynn ToolDefinition[] to Ollama Tool[] format.
*/
private convertTools(tools: ToolDefinition[]): Tool[] {
type OllamaParameter = {
type?: string | string[];
items?: unknown;
description?: string;
enum?: unknown[];
};
return tools.map(t => ({
type: 'function',
function: {
@@ -159,7 +165,7 @@ export class OllamaClient implements ModelClient {
parameters: {
type: t.input_schema.type,
required: t.input_schema.required,
properties: t.input_schema.properties as Record<string, any>,
properties: t.input_schema.properties as Record<string, OllamaParameter>,
},
},
}));
@@ -186,7 +192,7 @@ export class OllamaClient implements ModelClient {
// Extract content, checking for thinking field from reasoning models
let content = response.message.content;
let thinkingContent: string | undefined;
const thinking = (response.message as any).thinking;
const thinking = (response.message as unknown as { thinking?: unknown }).thinking;
if (thinking && typeof thinking === 'string') {
if (!content) {
// If no regular content, use thinking as content
@@ -245,7 +251,7 @@ export class OllamaClient implements ModelClient {
}
// Handle thinking field from reasoning models (e.g., deepseek-r1)
const thinking = (chunk.message as any)?.thinking;
const thinking = (chunk.message as unknown as { thinking?: unknown } | undefined)?.thinking;
if (thinking && typeof thinking === 'string') {
yield { type: 'content', content: thinking };
}
@@ -259,7 +265,7 @@ export class OllamaClient implements ModelClient {
if (chunk.done) {
// Handle tool_calls in the final chunk
const toolCalls = (chunk.message as any)?.tool_calls;
const toolCalls = (chunk.message as unknown as { tool_calls?: Array<{ function: { name: string; arguments: Record<string, unknown> } }> } | undefined)?.tool_calls;
if (toolCalls && Array.isArray(toolCalls)) {
for (let i = 0; i < toolCalls.length; i++) {
const tc = toolCalls[i];
+8 -4
View File
@@ -12,7 +12,7 @@ vi.mock('../auth/openai.js', () => ({
})),
}));
function makeSse(events: Array<{ event: string; data: any }>): string {
function makeSse(events: Array<{ event: string; data: unknown }>): string {
return events
.map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`)
.join('');
@@ -39,8 +39,12 @@ describe('OpenAIClient OAuth (Codex)', () => {
{ event: 'response.completed', data: { type: 'response.completed', response: { usage: { input_tokens: 2, output_tokens: 2 } } } },
]);
globalThis.fetch = vi.fn(async (_url: any, init?: any) => {
const parsed = JSON.parse(init.body);
globalThis.fetch = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
const body = typeof init?.body === 'string' ? init.body : '';
if (!body) {
throw new Error('Expected JSON body');
}
const parsed = JSON.parse(body) as Record<string, unknown>;
expect(parsed.store).toBe(false);
expect(parsed.stream).toBe(true);
expect(typeof parsed.instructions).toBe('string');
@@ -54,7 +58,7 @@ describe('OpenAIClient OAuth (Codex)', () => {
});
return new Response(stream, { status: 200 });
}) as any;
}) as typeof fetch;
const client = new OpenAIClient({ model: 'gpt-5.3-codex', useOAuth: true });
const resp = await client.chat({
+23 -8
View File
@@ -27,13 +27,22 @@ function toOpenAIContent(content: string | MessageContentPart[]): string | OpenA
return { type: 'text', text: part.text };
}
if (part.type === 'image') {
if (part.source.type === 'base64' && !part.source.data) {
return { type: 'text', text: '[Image omitted: missing base64 data]' };
}
if (part.source.type !== 'base64' && !part.source.url) {
return { type: 'text', text: '[Image omitted: missing URL]' };
}
// OpenAI accepts data URIs or regular URLs
const url = part.source.type === 'base64'
? `data:${part.source.media_type};base64,${part.source.data!}`
: part.source.url!;
? `data:${part.source.media_type};base64,${part.source.data}`
: part.source.url;
return { type: 'image_url', image_url: { url } };
}
if (part.type === 'audio') {
if (!part.source.data) {
return { type: 'text', text: '[Audio omitted: missing data]' };
}
// OpenAI native audio input via input_audio content part
// Determine format from MIME type (OpenAI supports: wav, mp3, flac, opus, ogg, webm)
const formatMap: Record<string, string> = {
@@ -157,9 +166,13 @@ export class OpenAIClient implements ModelClient {
}
}
if (!data) {return;}
let obj: any;
let obj: Record<string, unknown>;
try {
obj = JSON.parse(data);
const parsed = JSON.parse(data) as unknown;
if (!parsed || typeof parsed !== 'object') {
return;
}
obj = parsed as Record<string, unknown>;
} catch {
return;
}
@@ -169,8 +182,9 @@ export class OpenAIClient implements ModelClient {
}
if (obj.type === 'response.completed') {
const u = obj.response?.usage;
if (u) {
const response = obj.response as { usage?: { input_tokens?: number; output_tokens?: number } } | undefined;
const u = response?.usage;
if (u && typeof u === 'object') {
usage = {
inputTokens: u.input_tokens ?? 0,
outputTokens: u.output_tokens ?? 0,
@@ -179,7 +193,8 @@ export class OpenAIClient implements ModelClient {
}
if (obj.type === 'response.failed') {
const detail = obj.response?.error?.message ?? 'OpenAI OAuth response failed';
const response = obj.response as { error?: { message?: string } } | undefined;
const detail = response?.error?.message ?? 'OpenAI OAuth response failed';
throw new Error(detail);
}
};
@@ -247,7 +262,7 @@ export class OpenAIClient implements ModelClient {
// Extended thinking/reasoning mode for o1/o3 models
if (request.thinking) {
(params as any).reasoning_effort = 'medium';
(params as OpenAI.ChatCompletionCreateParamsNonStreaming & { reasoning_effort?: 'low' | 'medium' | 'high' }).reasoning_effort = 'medium';
}
let response: OpenAI.ChatCompletion;
+21 -9
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { ModelRouter } from './router.js';
import type { ModelClient, ChatResponse, ChatStreamEvent } from './types.js';
@@ -314,10 +314,13 @@ describe('setClient and labels', () => {
const newFastClient = router.getClient('fast');
expect(newFastClient).toBeDefined();
if (!newFastClient) {
throw new Error('Expected fast client to be set');
}
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'fast');
expect(newFastClient!.chat).toHaveBeenCalled();
expect(newFastClient!.chat).toHaveBeenCalledTimes(1);
expect(newFastClient.chat).toHaveBeenCalled();
expect(newFastClient.chat).toHaveBeenCalledTimes(1);
expect(mockClient1.chat).toHaveBeenCalledTimes(1);
});
@@ -336,10 +339,13 @@ describe('setClient and labels', () => {
const newClient = router.getClient('complex');
expect(newClient).toBe(mockClient2);
if (!newClient) {
throw new Error('Expected complex client to be set');
}
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'complex');
expect(newClient!.chat).toHaveBeenCalled();
expect(newClient.chat).toHaveBeenCalled();
});
it('getLabel returns the label set by setClient', () => {
@@ -424,19 +430,25 @@ describe('setClient and labels', () => {
const initialFastClient = router.getClient('fast');
expect(initialFastClient).toBeDefined();
if (!initialFastClient) {
throw new Error('Expected initial fast client to exist');
}
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'fast');
expect(initialFastClient!.chat).toHaveBeenCalled();
expect(initialFastClient!.chat).toHaveBeenCalledTimes(1);
expect(initialFastClient.chat).toHaveBeenCalled();
expect(initialFastClient.chat).toHaveBeenCalledTimes(1);
router.setClient('fast', mockClient2, 'fast-replaced');
const newFastClient = router.getClient('fast');
if (!newFastClient) {
throw new Error('Expected replaced fast client to exist');
}
await router.chat({ messages: [{ role: 'user', content: 'Test' }] }, 'fast');
expect(newFastClient!.chat).toHaveBeenCalled();
expect(newFastClient!.chat).toHaveBeenCalledTimes(1);
expect(initialFastClient!.chat).toHaveBeenCalledTimes(1);
expect(newFastClient.chat).toHaveBeenCalled();
expect(newFastClient.chat).toHaveBeenCalledTimes(1);
expect(initialFastClient.chat).toHaveBeenCalledTimes(1);
});
it('strict tier mode disables fallback chain for that tier', async () => {