Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions src/api/providers/__tests__/bedrock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1572,10 +1572,55 @@ describe("AwsBedrockHandler", () => {

it("returns false for older / non-adaptive models", () => {
expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-6-v1")).toBe(false)
expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-6")).toBe(false)
expect(isAdaptiveThinkingModel("anthropic.claude-3-5-sonnet-20241022-v2:0")).toBe(false)
expect(isAdaptiveThinkingModel("amazon.nova-lite-v1:0")).toBe(false)
})
})
})

describe("abortSignal pass-through", () => {
it("should use metadata.abortSignal when provided in completePrompt", async () => {
const mockConverseCommand = vi.fn()
vi.mock("@aws-sdk/client-bedrock-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("@aws-sdk/client-bedrock-runtime")>()
return {
...actual,
BedrockRuntimeClient: vi.fn().mockReturnValue({ send: vi.fn().mockResolvedValue({ stream: null }) }),
}
})

const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
})

const controller = new AbortController()
await handler.completePrompt("Test prompt", { abortSignal: controller.signal })

expect(mockConverseCommand).toHaveBeenCalled()
})

it("should use default controller signal when metadata.abortSignal not provided", async () => {
const mockConverseCommand = vi.fn()
vi.mock("@aws-sdk/client-bedrock-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("@aws-sdk/client-bedrock-runtime")>()
return {
...actual,
BedrockRuntimeClient: vi.fn().mockReturnValue({ send: vi.fn().mockResolvedValue({ stream: null }) }),
}
})

const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
})

await handler.completePrompt("Test prompt")

expect(mockConverseCommand).toHaveBeenCalled()
})
})
Comment on lines +1580 to +1625

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Tests are non-functional: vi.mock() inside test cases has no effect.

Lines 1583 and 1606 call vi.mock() inside individual test functions. Vitest hoists vi.mock() calls to module scope before imports resolve, so calling vi.mock() inside a test (after imports have already been processed) has no effect. Additionally, mockConverseCommand is defined as a local variable (lines 1582, 1605) that is never connected to the actual ConverseCommand constructor, so the expectations at lines 1601 and 1623 will always fail.

The module-level mock infrastructure already exists (lines 22-37). Use the existing vi.mocked(ConverseCommand) and BedrockRuntimeClient mocks instead.

🔧 Recommended fix: use module-level mocks
 	describe("abortSignal pass-through", () => {
+		const mockConverseCommand = vi.mocked(ConverseCommand)
+
+		beforeEach(() => {
+			mockConverseCommand.mockClear()
+		})
+
 		it("should use metadata.abortSignal when provided in completePrompt", async () => {
-			const mockConverseCommand = vi.fn()
-			vi.mock("`@aws-sdk/client-bedrock-runtime`", async (importOriginal) => {
-				const actual = await importOriginal<typeof import("`@aws-sdk/client-bedrock-runtime`")>()
-				return {
-					...actual,
-					BedrockRuntimeClient: vi.fn().mockReturnValue({ send: vi.fn().mockResolvedValue({ stream: null }) }),
-				}
-			})
-
 			const handler = new AwsBedrockHandler({
 				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
 				awsAccessKey: "test",
 				awsSecretKey: "test",
 				awsRegion: "us-east-1",
 			})
 
+			// Get the mock send from the handler's client
+			const clientInstance =
+				vi.mocked(BedrockRuntimeClient).mock.results[vi.mocked(BedrockRuntimeClient).mock.results.length - 1]?.value
+			const mockSendFn = clientInstance?.send as ReturnType<typeof vi.fn>
+			mockSendFn.mockResolvedValueOnce({ output: [{ type: "message", content: [{ type: "text", text: "Response" }] }] })
+
 			const controller = new AbortController()
 			await handler.completePrompt("Test prompt", { abortSignal: controller.signal })
 
 			expect(mockConverseCommand).toHaveBeenCalled()
+			const commandArg = mockConverseCommand.mock.calls[0][0] as any
+			// The upstream contract passes abortSignal via client.send options, not the command itself
+			// Verify the command was created (signal is passed separately to send)
 		})
 
 		it("should use default controller signal when metadata.abortSignal not provided", async () => {
-			const mockConverseCommand = vi.fn()
-			vi.mock("`@aws-sdk/client-bedrock-runtime`", async (importOriginal) => {
-				const actual = await importOriginal<typeof import("`@aws-sdk/client-bedrock-runtime`")>()
-				return {
-					...actual,
-					BedrockRuntimeClient: vi.fn().mockReturnValue({ send: vi.fn().mockResolvedValue({ stream: null }) }),
-				}
-			})
-
 			const handler = new AwsBedrockHandler({
 				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
 				awsAccessKey: "test",
 				awsSecretKey: "test",
 				awsRegion: "us-east-1",
 			})
 
+			const clientInstance =
+				vi.mocked(BedrockRuntimeClient).mock.results[vi.mocked(BedrockRuntimeClient).mock.results.length - 1]?.value
+			const mockSendFn = clientInstance?.send as ReturnType<typeof vi.fn>
+			mockSendFn.mockResolvedValueOnce({ output: [{ type: "message", content: [{ type: "text", text: "Response" }] }] })
+
 			await handler.completePrompt("Test prompt")
 
 			expect(mockConverseCommand).toHaveBeenCalled()
 		})
 	})

Note: The Bedrock implementation passes the abortSignal option to client.send(), not in the ConverseCommand constructor. You may need to spy on the send call's second argument to fully verify the signal threading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/bedrock.spec.ts` around lines 1580 - 1625, Remove
the ineffective vi.mock() calls from inside both test cases (the ones mocking
"`@aws-sdk/client-bedrock-runtime`") since Vitest hoists vi.mock() to module scope
before imports resolve, making in-test calls have no effect. Instead, use the
existing module-level mocks already present in the test file: access
vi.mocked(ConverseCommand) and BedrockRuntimeClient to verify behavior. Remove
the unused mockConverseCommand variables from both test cases. Since the Bedrock
implementation passes abortSignal to client.send() rather than the
ConverseCommand constructor, verify the signal is correctly threaded through to
the send call's options argument rather than expecting it in the command
constructor.

})
28 changes: 28 additions & 0 deletions src/api/providers/__tests__/native-ollama.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,34 @@ describe("NativeOllamaHandler", () => {
}),
)
})

it("should use metadata.abortSignal when provided in completePrompt", async () => {
mockChat.mockResolvedValue({
message: { content: "Response with abort signal" },
})

const controller = new AbortController()
await handler.completePrompt("Test prompt", { abortSignal: controller.signal })

expect(mockChat).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.any(Object),
}),
)
// Verify the call was made (signal is passed via Ollama SDK options)
const callArgs = mockChat.mock.calls[0][0] as any
expect(callArgs).toBeDefined()
})

it("should work without metadata in completePrompt", async () => {
mockChat.mockResolvedValue({
message: { content: "Response without metadata" },
})

await handler.completePrompt("Test prompt")

expect(mockChat).toHaveBeenCalled()
})
})

describe("error handling", () => {
Expand Down
49 changes: 49 additions & 0 deletions src/api/providers/__tests__/openai-codex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,52 @@ describe("OpenAiCodexHandler.getModel", () => {
expect(model.info).toBeDefined()
})
})

describe("OpenAiCodexHandler.completePrompt", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("should use metadata.abortSignal when provided in completePrompt", async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
output: [{ type: "message", content: [{ type: "output_text", text: "Codex response" }] }],
}),
})
global.fetch = mockFetch as any

const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.3-codex" })
const controller = new AbortController()

await handler.completePrompt("Test prompt", { abortSignal: controller.signal })

expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("/responses"),
expect.objectContaining({
signal: controller.signal,
}),
)
})

it("should use default abortController signal when metadata.abortSignal not provided", async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({
output: [{ type: "message", content: [{ type: "output_text", text: "Codex response" }] }],
}),
})
global.fetch = mockFetch as any

const handler = new OpenAiCodexHandler({ apiModelId: "gpt-5.3-codex" })

await handler.completePrompt("Test prompt")

expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("/responses"),
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
)
})
})
47 changes: 45 additions & 2 deletions src/api/providers/__tests__/openai-native.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ describe("OpenAiNativeHandler", () => {
],
}),
expect.objectContaining({
signal: expect.any(Object),
signal: expect.any(AbortSignal),
}),
)
})
Expand Down Expand Up @@ -245,6 +245,49 @@ describe("OpenAiNativeHandler", () => {

expect(result).toBe("")
})

it("should use metadata.abortSignal when provided in completePrompt", async () => {
mockResponsesCreate.mockResolvedValue({
output: [
{
type: "message",
content: [{ type: "output_text", text: "This is the completion response" }],
},
],
})

const controller = new AbortController()
const result = await handler.completePrompt("Test prompt", { abortSignal: controller.signal })

expect(result).toBe("This is the completion response")
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
signal: controller.signal,
}),
)
})

it("should use default abortController signal when metadata.abortSignal not provided", async () => {
mockResponsesCreate.mockResolvedValue({
output: [
{
type: "message",
content: [{ type: "output_text", text: "This is the completion response" }],
},
],
})

const result = await handler.completePrompt("Test prompt")

expect(result).toBe("This is the completion response")
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
signal: handler["abortController"].signal,
}),
)
})
Comment on lines +271 to +290

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Test will crash when accessing undefined abortController.

Line 287 accesses handler["abortController"].signal after completePrompt has completed. The upstream implementation (openai-native.ts:1525-1585) sets this.abortController = undefined in its finally block, so by the time this assertion runs, handler["abortController"] is undefined, causing a TypeError: Cannot read properties of undefined (reading 'signal').

🔧 Recommended fix: check the mock call arguments directly
 	it("should use default abortController signal when metadata.abortSignal not provided", async () => {
 		mockResponsesCreate.mockResolvedValue({
 			output: [
 				{
 					type: "message",
 					content: [{ type: "output_text", text: "This is the completion response" }],
 				},
 			],
 		})
 
 		const result = await handler.completePrompt("Test prompt")
 
 		expect(result).toBe("This is the completion response")
 		expect(mockResponsesCreate).toHaveBeenCalledWith(
 			expect.any(Object),
 			expect.objectContaining({
-				signal: handler["abortController"].signal,
+				signal: expect.any(AbortSignal),
 			}),
 		)
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should use default abortController signal when metadata.abortSignal not provided", async () => {
mockResponsesCreate.mockResolvedValue({
output: [
{
type: "message",
content: [{ type: "output_text", text: "This is the completion response" }],
},
],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("This is the completion response")
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
signal: handler["abortController"].signal,
}),
)
})
it("should use default abortController signal when metadata.abortSignal not provided", async () => {
mockResponsesCreate.mockResolvedValue({
output: [
{
type: "message",
content: [{ type: "output_text", text: "This is the completion response" }],
},
],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("This is the completion response")
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/__tests__/openai-native.spec.ts` around lines 271 - 290,
The test is attempting to access handler["abortController"].signal in the expect
assertion after completePrompt has completed, but the upstream implementation
sets this.abortController to undefined in its finally block, causing a TypeError
when trying to read signal from undefined. Instead of accessing the
abortController after the method completes, capture the abortController signal
before calling completePrompt, store it in a variable, and then verify that the
mockResponsesCreate was called with a signal argument that matches the stored
reference.

})

describe("getModel", () => {
Expand Down Expand Up @@ -1624,7 +1667,7 @@ describe("GPT-5 streaming event coverage (additional)", () => {
store: false,
}),
expect.objectContaining({
signal: expect.any(Object),
signal: expect.any(AbortSignal),
}),
)
})
Expand Down
9 changes: 6 additions & 3 deletions src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH

const command = new ConverseStreamCommand(payload)
const response = await this.client.send(command, {
abortSignal: controller.signal,
abortSignal: metadata?.abortSignal ?? controller.signal,
})

if (!response.stream) {
Expand Down Expand Up @@ -793,7 +793,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, metadata?: ApiHandlerCreateMessageMetadata): Promise<string> {
const controller = new AbortController()
try {
const modelConfig = this.getModel()

Expand Down Expand Up @@ -835,7 +836,9 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}

const command = new ConverseCommand(payload)
const response = await this.client.send(command)
const response = await this.client.send(command, {
abortSignal: metadata?.abortSignal ?? controller.signal,
})

if (
response?.output?.message?.content &&
Expand Down
6 changes: 3 additions & 3 deletions src/api/providers/native-ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,14 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
chatOptions.num_ctx = this.options.ollamaNumCtx
}

// Create the actual API request promise
const stream = await client.chat({
const controller = new AbortController()
const stream = (await client.chat({
model: modelId,
messages: ollamaMessages,
stream: true,
options: chatOptions,
tools: this.convertToolsToOllama(metadata?.tools),
})
})) as any
Comment on lines +238 to +244

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the Ollama SDK type signature for the chat method
rg -A 10 'chat\s*\(' --type ts --iglob '**/node_modules/ollama/**/*.d.ts'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 214


🏁 Script executed:

# Check package.json for ollama dependency
cat package.json | grep -A 2 -B 2 '"ollama"'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 47


🏁 Script executed:

# Look at the actual code in native-ollama.ts around the problematic lines
cat -n src/api/providers/native-ollama.ts | sed -n '230,250p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 857


🏁 Script executed:

# Search for any comments or explanations around the type cast
rg -B 5 -A 5 'client\.chat.*as any' src/api/providers/native-ollama.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 47


🏁 Script executed:

# Check if there are any type definitions for Ollama chat in the codebase
rg -i 'chat.*function|chat.*method|OllamaChat' --type ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 3383


🏁 Script executed:

# Check package.json for ollama dependency version
rg '"ollama"' package.json -A 1 -B 1

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 47


🏁 Script executed:

# Look at how client is initialized to understand its type
rg -B 10 'const client' src/api/providers/native-ollama.ts | head -30

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 815


🏁 Script executed:

# Check the test file to see how the mock is set up
cat -n src/api/providers/__tests__/native-ollama.spec.ts | head -100

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 3425


🏁 Script executed:

# Search for imports of Ollama to understand the SDK
rg 'import.*ollama|from.*ollama' --type ts -i

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2079


🏁 Script executed:

# Check how the stream variable is used after line 244
cat -n src/api/providers/native-ollama.ts | sed -n '244,280p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1659


🏁 Script executed:

# Look for the actual return type of client.chat in the Ollama SDK
rg -i 'export.*chat|declare.*chat' --type ts src/

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1854


🏁 Script executed:

# Get the full function signature to understand the context better
cat -n src/api/providers/native-ollama.ts | sed -n '206,260p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2154


🏁 Script executed:

# Check package.json and package-lock.json for the exact Ollama version
cat package.json | grep -A 5 -B 5 'ollama' || cat package-lock.json | grep -A 10 '"ollama"' | head -20

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 115


🏁 Script executed:

# Search for any GitHub issues or comments about Ollama type casting
rg -i 'ollama.*type|any.*chat|stream.*any' --type ts -C 2

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 42748


🏁 Script executed:

# Check if there are type definitions or interfaces defined for Ollama chat response
rg -i 'interface.*chat|type.*chat.*response|ollama.*response' --type ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2450


🏁 Script executed:

# Get the full package.json to find ollama version
cat package.json | grep -A 50 '"dependencies"' | grep -E '"ollama"|"version"'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 47


🏁 Script executed:

# Look for any comments or issues related to type casting in the Ollama provider
cat -n src/api/providers/native-ollama.ts | sed -n '1,50p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 2613


🏁 Script executed:

# Check if there are any other cast to any in similar streaming contexts
rg '\)\s*as\s+any' src/api/providers --type ts -B 2 -A 2

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 16509


Add explanatory comment or remove the unexplained any type cast.

The client.chat(...) call on line 244 casts to any without explanation. While other providers in the codebase make similar casts (e.g., anthropic.ts), those include comments explaining the SDK limitation. Either:

  1. Add a comment documenting why the Ollama SDK requires this cast (e.g., missing or incomplete type exports for streaming responses), or
  2. Replace with a properly typed return if the Ollama SDK exports the correct type.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/native-ollama.ts` around lines 238 - 244, The client.chat()
call in the native-ollama.ts file includes an unexplained `as any` type cast.
Add a comment above or inline with the cast explaining why the Ollama SDK
requires this workaround (e.g., missing or incomplete type exports for streaming
responses), similar to how other providers like anthropic.ts document their type
casts. Alternatively, if the Ollama SDK exports the correct type for streaming
responses, replace the `any` cast with the proper type instead of using a
comment.


let totalInputTokens = 0
let totalOutputTokens = 0
Expand Down
14 changes: 8 additions & 6 deletions src/api/providers/openai-codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
// Make the request with retry on auth failure
for (let attempt = 0; attempt < 2; attempt++) {
try {
yield* this.executeRequest(requestBody, model, accessToken, metadata?.taskId)
yield* this.executeRequest(requestBody, model, accessToken, metadata?.taskId, metadata)
return
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
Expand Down Expand Up @@ -345,6 +345,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
model: OpenAiCodexModel,
accessToken: string,
taskId?: string,
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
// Create AbortController for cancellation
this.abortController = new AbortController()
Expand Down Expand Up @@ -374,7 +375,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
})

const stream = (await (client as any).responses.create(requestBody, {
signal: this.abortController.signal,
signal: metadata?.abortSignal ?? this.abortController.signal,
// If the SDK supports per-request overrides, ensure headers are present.
headers: codexHeaders,
})) as AsyncIterable<any>
Expand All @@ -399,7 +400,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
}
} catch (_sdkErr) {
// Fallback to manual SSE via fetch (Codex backend).
yield* this.makeCodexRequest(requestBody, model, accessToken, taskId)
yield* this.makeCodexRequest(requestBody, model, accessToken, taskId, metadata)
}
} finally {
this.abortController = undefined
Expand Down Expand Up @@ -492,6 +493,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
model: OpenAiCodexModel,
accessToken: string,
taskId?: string,
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
// Per the implementation guide: route to Codex backend with Bearer token
const url = `${CODEX_API_BASE_URL}/responses`
Expand All @@ -518,7 +520,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
method: "POST",
headers,
body: JSON.stringify(requestBody),
signal: this.abortController?.signal,
signal: metadata?.abortSignal ?? this.abortController?.signal,
})

if (!response.ok) {
Expand Down Expand Up @@ -1151,7 +1153,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
return this.lastResponseId
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, metadata?: ApiHandlerCreateMessageMetadata): Promise<string> {
this.abortController = new AbortController()

try {
Expand Down Expand Up @@ -1213,7 +1215,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
method: "POST",
headers,
body: JSON.stringify(requestBody),
signal: this.abortController.signal,
signal: metadata?.abortSignal ?? this.abortController.signal,
})

if (!response.ok) {
Expand Down
11 changes: 6 additions & 5 deletions src/api/providers/openai-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio

try {
// Use the official SDK with per-request headers
const signal = metadata?.abortSignal ?? this.abortController.signal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

metadata?.abortSignal is accessed but not declared in ApiHandlerCreateMessageMetadata.

All four provider handlers read metadata?.abortSignal, but the shared type contract ApiHandlerCreateMessageMetadata (defined in src/api/index.ts:47-93) does not declare an abortSignal field. This breaks type safety: TypeScript will not guide callers to pass an abortSignal, and the interface documentation does not reflect the actual usage.

  • src/api/providers/openai-native.ts#L427: Add abortSignal?: AbortSignal to ApiHandlerCreateMessageMetadata in src/api/index.ts.
  • src/api/providers/openai-native.ts#L580: Same fix applies here.
  • src/api/providers/openai-native.ts#L1548-L1550: Same fix applies here.
  • src/api/providers/openai-codex.ts#L378: Same fix applies here.
  • src/api/providers/openai-codex.ts#L523: Same fix applies here.
  • src/api/providers/openai-codex.ts#L1218: Same fix applies here.
  • src/api/providers/bedrock.ts#L547: Same fix applies here.
  • src/api/providers/bedrock.ts#L839-L841: Same fix applies here.
📍 Affects 3 files
  • src/api/providers/openai-native.ts#L427-L427 (this comment)
  • src/api/providers/openai-native.ts#L580-L580
  • src/api/providers/openai-native.ts#L1548-L1550
  • src/api/providers/openai-codex.ts#L378-L378
  • src/api/providers/openai-codex.ts#L523-L523
  • src/api/providers/openai-codex.ts#L1218-L1218
  • src/api/providers/bedrock.ts#L547-L547
  • src/api/providers/bedrock.ts#L839-L841
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/openai-native.ts` at line 427, The
`ApiHandlerCreateMessageMetadata` interface definition in `src/api/index.ts` is
missing the `abortSignal` field that all four provider handlers are accessing.
Add `abortSignal?: AbortSignal` to the `ApiHandlerCreateMessageMetadata`
interface in `src/api/index.ts` to restore type safety. This single fix at the
root interface will resolve the type issues across all affected call sites:
`src/api/providers/openai-native.ts` at lines 427, 580, and 1548-1550;
`src/api/providers/openai-codex.ts` at lines 378, 523, and 1218; and
`src/api/providers/bedrock.ts` at lines 547 and 839-841. No changes are needed
at the sibling locations—they will automatically align with the corrected
interface definition.

const stream = (await (this.client as any).responses.create(requestBody, {
signal: this.abortController.signal,
signal,
headers: requestHeaders,
})) as AsyncIterable<any>

Expand Down Expand Up @@ -576,7 +577,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
"User-Agent": userAgent,
},
body: JSON.stringify(requestBody),
signal: this.abortController.signal,
signal: metadata?.abortSignal ?? this.abortController.signal,
})

if (!response.ok) {
Expand Down Expand Up @@ -1482,7 +1483,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
return this.lastResponseId
}

async completePrompt(prompt: string): Promise<string> {
async completePrompt(prompt: string, metadata?: ApiHandlerCreateMessageMetadata): Promise<string> {
// Create AbortController for cancellation
this.abortController = new AbortController()

Expand Down Expand Up @@ -1544,9 +1545,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
requestBody.prompt_cache_retention = promptCacheRetention
}

// Make the non-streaming request
// Make the non-streaming request with conditional signal pass-through
const response = await (this.client as any).responses.create(requestBody, {
signal: this.abortController.signal,
signal: metadata?.abortSignal ?? this.abortController.signal,
})

// Extract text from the response
Expand Down
Loading