Roo-Code/src/api/providers/__tests__/ollama.spec.ts
roomote[bot] 63b71d8299
feat: add Ollama API key support for Turbo mode (#7425)
* feat: add Ollama API key support for Turbo mode

- Add ollamaApiKey field to ProviderSettings schema
- Add ollamaApiKey to SECRET_STATE_KEYS for secure storage
- Update Ollama and NativeOllama providers to use API key for authentication
- Add UI field for Ollama API key (shown when custom base URL is provided)
- Add test coverage for API key functionality

This enables users to use Ollama Turbo with datacenter-grade hardware by providing an API key for authenticated Ollama instances or cloud services.

* fix: use VSCodeTextField for Ollama API key field

Remove non-existent ApiKeyField import and use standard VSCodeTextField with password type, matching other provider implementations

* Add missing translation keys for Ollama API key support

- Add providers.ollama.apiKey and providers.ollama.apiKeyHelp to all 18 language files
- Support for authenticated Ollama instances and cloud services
- Relates to PR #7425

* refactor: improve type safety for Ollama client configuration

- Replace 'any' type with proper OllamaOptions (Config) type
- Import Config type from ollama package for better type checking

---------

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
2025-08-30 19:20:31 -04:00

178 lines
4.5 KiB
TypeScript

// npx vitest run api/providers/__tests__/ollama.spec.ts
import { Anthropic } from "@anthropic-ai/sdk"
import { OllamaHandler } from "../ollama"
import { ApiHandlerOptions } from "../../../shared/api"
const mockCreate = vitest.fn()
vitest.mock("openai", () => {
return {
__esModule: true,
default: vitest.fn().mockImplementation(() => ({
chat: {
completions: {
create: mockCreate.mockImplementation(async (options) => {
if (!options.stream) {
return {
id: "test-completion",
choices: [
{
message: { role: "assistant", content: "Test response" },
finish_reason: "stop",
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
}
return {
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { content: "Test response" },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}
}),
},
},
})),
}
})
describe("OllamaHandler", () => {
let handler: OllamaHandler
let mockOptions: ApiHandlerOptions
beforeEach(() => {
mockOptions = {
apiModelId: "llama2",
ollamaModelId: "llama2",
ollamaBaseUrl: "http://localhost:11434/v1",
}
handler = new OllamaHandler(mockOptions)
mockCreate.mockClear()
})
describe("constructor", () => {
it("should initialize with provided options", () => {
expect(handler).toBeInstanceOf(OllamaHandler)
expect(handler.getModel().id).toBe(mockOptions.ollamaModelId)
})
it("should use default base URL if not provided", () => {
const handlerWithoutUrl = new OllamaHandler({
apiModelId: "llama2",
ollamaModelId: "llama2",
})
expect(handlerWithoutUrl).toBeInstanceOf(OllamaHandler)
})
it("should use API key when provided", () => {
const handlerWithApiKey = new OllamaHandler({
apiModelId: "llama2",
ollamaModelId: "llama2",
ollamaBaseUrl: "https://ollama.com",
ollamaApiKey: "test-api-key",
})
expect(handlerWithApiKey).toBeInstanceOf(OllamaHandler)
// The API key will be used in the Authorization header
})
})
describe("createMessage", () => {
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Hello!",
},
]
it("should handle streaming responses", async () => {
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(chunks.length).toBeGreaterThan(0)
const textChunks = chunks.filter((chunk) => chunk.type === "text")
expect(textChunks).toHaveLength(1)
expect(textChunks[0].text).toBe("Test response")
})
it("should handle API errors", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
const stream = handler.createMessage(systemPrompt, messages)
await expect(async () => {
for await (const _chunk of stream) {
// Should not reach here
}
}).rejects.toThrow("API Error")
})
})
describe("completePrompt", () => {
it("should complete prompt successfully", async () => {
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("Test response")
expect(mockCreate).toHaveBeenCalledWith({
model: mockOptions.ollamaModelId,
messages: [{ role: "user", content: "Test prompt" }],
temperature: 0,
stream: false,
})
})
it("should handle API errors", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Ollama completion error: API Error")
})
it("should handle empty response", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: "" } }],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
})
})
describe("getModel", () => {
it("should return model info", () => {
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe(mockOptions.ollamaModelId)
expect(modelInfo.info).toBeDefined()
expect(modelInfo.info.maxTokens).toBe(-1)
expect(modelInfo.info.contextWindow).toBe(128_000)
})
})
})