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
58 changes: 58 additions & 0 deletions src/api/providers/__tests__/native-ollama.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,64 @@ describe("NativeOllamaHandler", () => {
})
expect(result).toBe("This is the response")
})

it("should not override num_ctx in options", async () => {
mockChat.mockResolvedValue({
message: { content: "Response" },
})

await handler.completePrompt("Test prompt")

// Verify that num_ctx is NOT in the options
expect(mockChat).toHaveBeenCalledWith({
model: "llama2",
messages: [{ role: "user", content: "Test prompt" }],
stream: false,
options: {
temperature: 0,
// num_ctx should NOT be present here
},
})

// Explicitly check that num_ctx is not in the options
const callArgs = mockChat.mock.calls[0][0]
expect(callArgs.options).not.toHaveProperty("num_ctx")
})
})

describe("createMessage num_ctx handling", () => {
it("should not set num_ctx in options for createMessage", async () => {
// Mock the chat response
mockChat.mockImplementation(async function* () {
yield {
message: { content: "Test" },
eval_count: 1,
prompt_eval_count: 1,
}
})

const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }])

// Consume the stream
for await (const _ of stream) {
// Just consume
}

// Verify the call was made without num_ctx
expect(mockChat).toHaveBeenCalledWith({
model: "llama2",
messages: expect.any(Array),
stream: true,
options: {
temperature: 0,
// num_ctx should NOT be present
},
})

// Explicitly verify num_ctx is not in options
const callArgs = mockChat.mock.calls[0][0]
expect(callArgs.options).not.toHaveProperty("num_ctx")
})
})

describe("error handling", () => {
Expand Down
34 changes: 34 additions & 0 deletions src/api/providers/fetchers/__tests__/ollama.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,40 @@ describe("Ollama Fetcher", () => {
})
})

it("should parse num_ctx from parameters field when present", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice test coverage! Though it might be worth adding edge case tests for:

  • Invalid num_ctx values (negative, zero, non-numeric)
  • Very large values
  • Malformed parameters string
  • Multiple num_ctx entries (which one wins?)

const modelDataWithNumCtx = {
...ollamaModelsData["qwen3-2to16:latest"],
parameters: "num_ctx 16384\nstop_token <eos>",
model_info: {
"ollama.context_length": 40960,
},
}

const parsedModel = parseOllamaModel(modelDataWithNumCtx as any)

// Should use the configured num_ctx (16384) instead of the default context_length (40960)
expect(parsedModel.contextWindow).toBe(16384)
expect(parsedModel.maxTokens).toBe(16384)
expect(parsedModel.description).toBe("Family: qwen3, Context: 16384, Size: 32.8B")
})

it("should use default context_length when num_ctx is not in parameters", () => {
const modelDataWithoutNumCtx = {
...ollamaModelsData["qwen3-2to16:latest"],
parameters: "stop_token <eos>", // No num_ctx here
model_info: {
"ollama.context_length": 40960,
},
}

const parsedModel = parseOllamaModel(modelDataWithoutNumCtx as any)

// Should use the default context_length (40960)
expect(parsedModel.contextWindow).toBe(40960)
expect(parsedModel.maxTokens).toBe(40960)
expect(parsedModel.description).toBe("Family: qwen3, Context: 40960, Size: 32.8B")
})

it("should handle models with null families field", () => {
const modelDataWithNullFamilies = {
...ollamaModelsData["qwen3-2to16:latest"],
Expand Down
22 changes: 18 additions & 4 deletions src/api/providers/fetchers/ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,30 @@ type OllamaModelInfoResponse = z.infer<typeof OllamaModelInfoResponseSchema>

export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo => {
const contextKey = Object.keys(rawModel.model_info).find((k) => k.includes("context_length"))
const contextWindow =
const defaultContextWindow =
contextKey && typeof rawModel.model_info[contextKey] === "number" ? rawModel.model_info[contextKey] : undefined

// Parse the parameters field to check for user-configured num_ctx
let configuredNumCtx: number | undefined
if (rawModel.parameters) {
// The parameters field contains modelfile parameters as a string
// Look for num_ctx setting in the format "num_ctx <value>"
const numCtxMatch = rawModel.parameters.match(/num_ctx\s+(\d+)/i)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this regex pattern comprehensive enough? The current pattern /num_ctx\s+(\d+)/i might miss valid modelfile formats like:

  • Values with underscores: num_ctx 32_768
  • Scientific notation: num_ctx 1e5

Might want to consider a more flexible pattern or at least document the expected format.

if (numCtxMatch && numCtxMatch[1]) {
configuredNumCtx = parseInt(numCtxMatch[1], 10)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we validate the parsed num_ctx value here? An extremely large value (like Number.MAX_SAFE_INTEGER) or zero could cause issues. Consider adding bounds checking:

Suggested change
configuredNumCtx = parseInt(numCtxMatch[1], 10)
if (numCtxMatch && numCtxMatch[1]) {
const parsed = parseInt(numCtxMatch[1], 10)
// Validate reasonable bounds (e.g., 512 to 1M tokens)
if (parsed >= 512 && parsed <= 1048576) {
configuredNumCtx = parsed
}
}

}
}

// Use the configured num_ctx if available, otherwise fall back to the default
const actualContextWindow = configuredNumCtx || defaultContextWindow || ollamaDefaultModelInfo.contextWindow

const modelInfo: ModelInfo = Object.assign({}, ollamaDefaultModelInfo, {
description: `Family: ${rawModel.details.family}, Context: ${contextWindow}, Size: ${rawModel.details.parameter_size}`,
contextWindow: contextWindow || ollamaDefaultModelInfo.contextWindow,
description: `Family: ${rawModel.details.family}, Context: ${actualContextWindow}, Size: ${rawModel.details.parameter_size}`,
contextWindow: actualContextWindow,
supportsPromptCache: true,
supportsImages: rawModel.capabilities?.includes("vision"),
supportsComputerUse: false,
maxTokens: contextWindow || ollamaDefaultModelInfo.contextWindow,
maxTokens: actualContextWindow,
})

return modelInfo
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/native-ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
messages: ollamaMessages,
stream: true,
options: {
num_ctx: modelInfo.contextWindow,
// Don't override num_ctx - let Ollama use the model's configured value
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consider adding a more detailed comment explaining why we're not setting num_ctx and how Ollama handles the default. Future maintainers (including future me) might wonder why this was removed.

temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
},
})
Expand Down
Loading