Skip to content

Commit 4008a1a

Browse files
Modifying the usage of unbound.ts in compliance with all providers
1 parent 31ec687 commit 4008a1a

File tree

2 files changed

+299
-73
lines changed

2 files changed

+299
-73
lines changed
Lines changed: 186 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,210 @@
11
import { UnboundHandler } from "../unbound"
22
import { ApiHandlerOptions } from "../../../shared/api"
3-
import fetchMock from "jest-fetch-mock"
3+
import OpenAI from "openai"
4+
import { Anthropic } from "@anthropic-ai/sdk"
45

5-
fetchMock.enableMocks()
6+
// Mock OpenAI client
7+
const mockCreate = jest.fn()
8+
const mockWithResponse = jest.fn()
69

7-
describe("UnboundHandler", () => {
8-
const mockOptions: ApiHandlerOptions = {
9-
unboundApiKey: "test-api-key",
10-
apiModelId: "test-model-id",
10+
jest.mock("openai", () => {
11+
return {
12+
__esModule: true,
13+
default: jest.fn().mockImplementation(() => ({
14+
chat: {
15+
completions: {
16+
create: (...args: any[]) => {
17+
const stream = {
18+
[Symbol.asyncIterator]: async function* () {
19+
yield {
20+
choices: [
21+
{
22+
delta: { content: "Test response" },
23+
index: 0,
24+
},
25+
],
26+
}
27+
yield {
28+
choices: [
29+
{
30+
delta: {},
31+
index: 0,
32+
},
33+
],
34+
}
35+
},
36+
}
37+
38+
const result = mockCreate(...args)
39+
if (args[0].stream) {
40+
mockWithResponse.mockReturnValue(
41+
Promise.resolve({
42+
data: stream,
43+
response: { headers: new Map() },
44+
}),
45+
)
46+
result.withResponse = mockWithResponse
47+
}
48+
return result
49+
},
50+
},
51+
},
52+
})),
1153
}
54+
})
55+
56+
describe("UnboundHandler", () => {
57+
let handler: UnboundHandler
58+
let mockOptions: ApiHandlerOptions
1259

1360
beforeEach(() => {
14-
fetchMock.resetMocks()
61+
mockOptions = {
62+
apiModelId: "anthropic/claude-3-5-sonnet-20241022",
63+
unboundApiKey: "test-api-key",
64+
}
65+
handler = new UnboundHandler(mockOptions)
66+
mockCreate.mockClear()
67+
mockWithResponse.mockClear()
68+
69+
// Default mock implementation for non-streaming responses
70+
mockCreate.mockResolvedValue({
71+
id: "test-completion",
72+
choices: [
73+
{
74+
message: { role: "assistant", content: "Test response" },
75+
finish_reason: "stop",
76+
index: 0,
77+
},
78+
],
79+
})
1580
})
1681

17-
it("should initialize with options", () => {
18-
const handler = new UnboundHandler(mockOptions)
19-
expect(handler).toBeDefined()
82+
describe("constructor", () => {
83+
it("should initialize with provided options", () => {
84+
expect(handler).toBeInstanceOf(UnboundHandler)
85+
expect(handler.getModel().id).toBe(mockOptions.apiModelId)
86+
})
2087
})
2188

22-
it("should create a message successfully", async () => {
23-
const handler = new UnboundHandler(mockOptions)
24-
const mockResponse = {
25-
choices: [{ message: { content: "Hello, world!" } }],
26-
usage: { prompt_tokens: 5, completion_tokens: 7 },
27-
}
89+
describe("createMessage", () => {
90+
const systemPrompt = "You are a helpful assistant."
91+
const messages: Anthropic.Messages.MessageParam[] = [
92+
{
93+
role: "user",
94+
content: "Hello!",
95+
},
96+
]
2897

29-
fetchMock.mockResponseOnce(JSON.stringify(mockResponse))
98+
it("should handle streaming responses", async () => {
99+
const stream = handler.createMessage(systemPrompt, messages)
100+
const chunks: any[] = []
101+
for await (const chunk of stream) {
102+
chunks.push(chunk)
103+
}
30104

31-
const generator = handler.createMessage("system prompt", [])
32-
const textResult = await generator.next()
33-
const usageResult = await generator.next()
105+
expect(chunks.length).toBe(1)
106+
expect(chunks[0]).toEqual({
107+
type: "text",
108+
text: "Test response",
109+
})
34110

35-
expect(textResult.value).toEqual({ type: "text", text: "Hello, world!" })
36-
expect(usageResult.value).toEqual({
37-
type: "usage",
38-
inputTokens: 5,
39-
outputTokens: 7,
111+
expect(mockCreate).toHaveBeenCalledWith(
112+
expect.objectContaining({
113+
model: "claude-3-5-sonnet-20241022",
114+
messages: expect.any(Array),
115+
stream: true,
116+
}),
117+
expect.objectContaining({
118+
headers: {
119+
"X-Unbound-Metadata": expect.stringContaining("roo-code"),
120+
},
121+
}),
122+
)
40123
})
41-
})
42124

43-
it("should handle API errors", async () => {
44-
const handler = new UnboundHandler(mockOptions)
45-
fetchMock.mockResponseOnce(JSON.stringify({ error: "API error" }), { status: 400 })
125+
it("should handle API errors", async () => {
126+
mockCreate.mockImplementationOnce(() => {
127+
throw new Error("API Error")
128+
})
129+
130+
const stream = handler.createMessage(systemPrompt, messages)
131+
const chunks = []
46132

47-
const generator = handler.createMessage("system prompt", [])
48-
await expect(generator.next()).rejects.toThrow("Unbound Gateway completion error: API error")
133+
try {
134+
for await (const chunk of stream) {
135+
chunks.push(chunk)
136+
}
137+
fail("Expected error to be thrown")
138+
} catch (error) {
139+
expect(error).toBeInstanceOf(Error)
140+
expect(error.message).toBe("API Error")
141+
}
142+
})
49143
})
50144

51-
it("should handle network errors", async () => {
52-
const handler = new UnboundHandler(mockOptions)
53-
fetchMock.mockRejectOnce(new Error("Network error"))
145+
describe("completePrompt", () => {
146+
it("should complete prompt successfully", async () => {
147+
const result = await handler.completePrompt("Test prompt")
148+
expect(result).toBe("Test response")
149+
expect(mockCreate).toHaveBeenCalledWith(
150+
expect.objectContaining({
151+
model: "claude-3-5-sonnet-20241022",
152+
messages: [{ role: "user", content: "Test prompt" }],
153+
temperature: 0,
154+
max_tokens: 8192,
155+
}),
156+
)
157+
})
158+
159+
it("should handle API errors", async () => {
160+
mockCreate.mockRejectedValueOnce(new Error("API Error"))
161+
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Unbound completion error: API Error")
162+
})
163+
164+
it("should handle empty response", async () => {
165+
mockCreate.mockResolvedValueOnce({
166+
choices: [{ message: { content: "" } }],
167+
})
168+
const result = await handler.completePrompt("Test prompt")
169+
expect(result).toBe("")
170+
})
171+
172+
it("should not set max_tokens for non-Anthropic models", async () => {
173+
mockCreate.mockClear()
174+
175+
const nonAnthropicOptions = {
176+
apiModelId: "openai/gpt-4o",
177+
unboundApiKey: "test-key",
178+
}
179+
const nonAnthropicHandler = new UnboundHandler(nonAnthropicOptions)
54180

55-
const generator = handler.createMessage("system prompt", [])
56-
await expect(generator.next()).rejects.toThrow("Unbound Gateway completion error: Network error")
181+
await nonAnthropicHandler.completePrompt("Test prompt")
182+
expect(mockCreate).toHaveBeenCalledWith(
183+
expect.objectContaining({
184+
model: "gpt-4o",
185+
messages: [{ role: "user", content: "Test prompt" }],
186+
temperature: 0,
187+
}),
188+
)
189+
expect(mockCreate.mock.calls[0][0]).not.toHaveProperty("max_tokens")
190+
})
57191
})
58192

59-
it("should return the correct model", () => {
60-
const handler = new UnboundHandler(mockOptions)
61-
const model = handler.getModel()
62-
expect(model.id).toBe("gpt-4o")
193+
describe("getModel", () => {
194+
it("should return model info", () => {
195+
const modelInfo = handler.getModel()
196+
expect(modelInfo.id).toBe(mockOptions.apiModelId)
197+
expect(modelInfo.info).toBeDefined()
198+
})
199+
200+
it("should return default model when invalid model provided", () => {
201+
const handlerWithInvalidModel = new UnboundHandler({
202+
...mockOptions,
203+
apiModelId: "invalid/model",
204+
})
205+
const modelInfo = handlerWithInvalidModel.getModel()
206+
expect(modelInfo.id).toBe("openai/gpt-4o") // Default model
207+
expect(modelInfo.info).toBeDefined()
208+
})
63209
})
64210
})

0 commit comments

Comments
 (0)