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
79 changes: 79 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## Fix: contextWindow and maxTokens not read from OpenAI-compatible servers

### maxTokens Issue

**Problem**: The generic OpenAI-compatible adapter always used a hardcoded default of 4096 for `maxTokens`, even when servers reported `max_completion_tokens` in the `/v1/models` response. This was way too small for modern models with large context windows.

**Fix**:
1. First determine the `contextWindow` (checking `context_window`, `max_model_len`, `context_length`, `max_context_length`)
2. Try to read `max_completion_tokens` from the server response
3. If the server provides it, use that value
4. Otherwise, default to **half the context window** (reasonable balance between input and output)

### contextWindow Issue

### Problem

The generic OpenAI-compatible adapter in `src/adapters/generic.ts` only checks for these context-window fields in the `/v1/models` response:

- `context_window`
- `context_length`
- `max_context_length`

Many servers (e.g. **omlx**) report the value under **`max_model_len`** instead. When this field is encountered, `contextWindow` stays `undefined` and a default fallback is used, giving wrong values to the user.

### Fix

In `src/adapters/generic.ts`:

1. **Add `max_model_len` to the response type** so TypeScript recognizes the field.
2. **Insert `max_model_len` into the context-window detection chain** as the second check (after `context_window`, before `context_length` and `max_context_length`).

### Files changed

- `src/adapters/generic.ts`

### Diff

```diff
--- a/src/adapters/generic.ts
+++ b/src/adapters/generic.ts
@@ -122,7 +122,7 @@
headers["Authorization"] = `Bearer ${cred.apiKey}`;
}

- const body = r.json as { data?: Array<{ id?: unknown; max_completion_tokens?: number; context_length?: number; max_context_length?: number; context_window?: number }> } | undefined;
+ const body = r.json as { data?: Array<{ id?: unknown; max_completion_tokens?: number; max_model_len?: number; context_length?: number; max_context_length?: number; context_window?: number }> } | undefined;
if (!Array.isArray(body?.data)) return [];

return body.data
@@ -135,13 +135,18 @@
const contextWindow =
typeof item.context_window === "number" && item.context_window > 0
? item.context_window
+ : typeof item.max_model_len === "number" && item.max_model_len > 0
+ ? item.max_model_len
: typeof item.context_length === "number" && item.context_length > 0
? item.context_length
: typeof item.max_context_length === "number" && item.max_context_length > 0
? item.max_context_length
: undefined;

+ const finalContextWindow = contextWindow ?? DEFAULT_CONTEXT_WINDOW;
+
const maxTokens =
typeof item.max_completion_tokens === "number" && item.max_completion_tokens > 0
? item.max_completion_tokens
+ : Math.floor(finalContextWindow / 2);

return {
id: item.id,
name: item.id,
- contextWindow: contextWindow ?? DEFAULT_CONTEXT_WINDOW,
- maxTokens: DEFAULT_MAX_TOKENS,
+ contextWindow: finalContextWindow,
+ maxTokens,
input: ["text"],
reasoning: false,
embeddings: isEmbedding,
```
45 changes: 0 additions & 45 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 36 additions & 5 deletions src/adapters/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class GenericAdapter implements BackendAdapter {

/**
* GET /v1/models → map data[].id to ModelDescriptor.
* Conservative defaults are applied (contextWindow 8192, maxTokens 4096, input ["text"]).
* Conservative defaults are applied (contextWindow 8192, maxTokens = half of contextWindow, input ["text"]).
* Common embedding/reranking model families are excluded from chat registration.
* Throws on non-ok / 401 / status:0.
*/
Expand All @@ -112,21 +112,52 @@ class GenericAdapter implements BackendAdapter {
if (r.status === 0) throw new Error("listModels failed: server unreachable (status 0)");
if (!r.ok) throw new Error(`listModels failed: HTTP ${r.status}`);

const body = r.json as { data?: Array<{ id?: unknown }> } | undefined;
interface GenericModelEntry {
id?: unknown;
max_completion_tokens?: number;
max_model_len?: number;
context_length?: number;
max_context_length?: number;
context_window?: number;
}
interface GenericModelEntryWithId extends GenericModelEntry {
id: string;
}

const body = r.json as { data?: GenericModelEntry[] } | undefined;
if (!Array.isArray(body?.data)) return [];

return body.data
.filter((item): item is { id: string } => typeof item?.id === "string")
.filter((item): item is GenericModelEntryWithId => typeof item?.id === "string")
.map((item): ModelDescriptor => {
const normalizedId = item.id.toLowerCase();
const isEmbedding =
/(^|[/:._-])(embed|embedding|bge|gte|e5|reranker)([/:._-]|$)/.test(normalizedId) ||
normalizedId.includes("nomic-embed");

const contextWindow =
typeof item.context_window === "number" && item.context_window > 0
? item.context_window
: typeof item.max_model_len === "number" && item.max_model_len > 0
? item.max_model_len
: typeof item.context_length === "number" && item.context_length > 0
? item.context_length
: typeof item.max_context_length === "number" && item.max_context_length > 0
? item.max_context_length
: undefined;

const finalContextWindow = contextWindow ?? DEFAULT_CONTEXT_WINDOW;

const maxTokens =
typeof item.max_completion_tokens === "number" && item.max_completion_tokens > 0
? item.max_completion_tokens
: Math.floor(finalContextWindow / 2);

return {
id: item.id,
name: item.id,
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
contextWindow: finalContextWindow,
maxTokens,
input: ["text"],
reasoning: false,
embeddings: isEmbedding,
Expand Down
3 changes: 3 additions & 0 deletions src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { vllmAdapter } from "./vllm.ts";
import { omlxAdapter } from "./omlx.ts";
import { openaiAdapter } from "./openai.ts";
import { anthropicAdapter } from "./anthropic.ts";
import { unslothAdapter } from "./unsloth.ts";
import { genericAdapter } from "./generic.ts";

/** Every adapter Crossbar ships. */
Expand All @@ -31,6 +32,7 @@ export const ADAPTERS: readonly BackendAdapter[] = [
omlxAdapter,
openaiAdapter,
anthropicAdapter,
unslothAdapter,
genericAdapter,
];

Expand Down Expand Up @@ -63,5 +65,6 @@ export {
omlxAdapter,
openaiAdapter,
anthropicAdapter,
unslothAdapter,
genericAdapter,
};
Loading