mirror of
https://github.com/strands-agents/harness-sdk.git
synced 2026-10-02 02:44:48 +08:00
feat: auto-populate contextWindowLimit from model ID lookup tables (#954)
This commit is contained in:
@@ -136,6 +136,45 @@ describe('AnthropicModel', () => {
|
||||
expect.stringContaining('using default modelId')
|
||||
)
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit from model ID lookup', () => {
|
||||
const provider = new AnthropicModel({ apiKey: 'sk-test', modelId: 'claude-sonnet-4-20250514' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'claude-sonnet-4-20250514',
|
||||
maxTokens: 64_000,
|
||||
contextWindowLimit: 1_000_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit for default model ID', () => {
|
||||
const provider = new AnthropicModel({ apiKey: 'sk-test' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'claude-sonnet-4-6',
|
||||
maxTokens: 64_000,
|
||||
contextWindowLimit: 1_000_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not override explicit contextWindowLimit', () => {
|
||||
const provider = new AnthropicModel({
|
||||
apiKey: 'sk-test',
|
||||
modelId: 'claude-sonnet-4-20250514',
|
||||
contextWindowLimit: 100_000,
|
||||
})
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'claude-sonnet-4-20250514',
|
||||
maxTokens: 64_000,
|
||||
contextWindowLimit: 100_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves contextWindowLimit undefined for unknown model IDs', () => {
|
||||
const provider = new AnthropicModel({ apiKey: 'sk-test', modelId: 'unknown-model' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'unknown-model',
|
||||
maxTokens: 64_000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateConfig', () => {
|
||||
@@ -147,6 +186,22 @@ describe('AnthropicModel', () => {
|
||||
maxTokens: 8192,
|
||||
})
|
||||
})
|
||||
|
||||
it('re-resolves contextWindowLimit when modelId changes and it was auto-resolved', () => {
|
||||
const provider = new AnthropicModel({ apiKey: 'sk-test' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_000_000) // claude-sonnet-4-6 default
|
||||
|
||||
provider.updateConfig({ modelId: 'claude-sonnet-4-20250514' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_000_000) // claude-sonnet-4-20250514 value
|
||||
})
|
||||
|
||||
it('preserves explicit contextWindowLimit when modelId changes', () => {
|
||||
const provider = new AnthropicModel({ apiKey: 'sk-test', contextWindowLimit: 50_000 })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000)
|
||||
|
||||
provider.updateConfig({ modelId: 'claude-sonnet-4-20250514' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000) // preserved
|
||||
})
|
||||
})
|
||||
|
||||
describe('stream event handling', () => {
|
||||
|
||||
@@ -208,6 +208,7 @@ describe('BedrockModel', () => {
|
||||
const provider = new BedrockModel({ modelId: customModelId })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: customModelId,
|
||||
contextWindowLimit: 200_000,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -292,6 +293,7 @@ describe('BedrockModel', () => {
|
||||
expect(config).toStrictEqual({
|
||||
modelId: 'global.anthropic.claude-sonnet-4-6',
|
||||
temperature: 0.5,
|
||||
contextWindowLimit: 1_000_000,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -305,6 +307,48 @@ describe('BedrockModel', () => {
|
||||
contextWindowLimit: 200_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit from model ID lookup', () => {
|
||||
const provider = new BedrockModel({ modelId: 'anthropic.claude-sonnet-4-20250514-v1:0' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
contextWindowLimit: 1_000_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit for cross-region model IDs', () => {
|
||||
const provider = new BedrockModel({ modelId: 'us.anthropic.claude-sonnet-4-6' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'us.anthropic.claude-sonnet-4-6',
|
||||
contextWindowLimit: 1_000_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit for default model ID', () => {
|
||||
const provider = new BedrockModel()
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'global.anthropic.claude-sonnet-4-6',
|
||||
contextWindowLimit: 1_000_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not override explicit contextWindowLimit', () => {
|
||||
const provider = new BedrockModel({
|
||||
modelId: 'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
contextWindowLimit: 100_000,
|
||||
})
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
contextWindowLimit: 100_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves contextWindowLimit undefined for unknown model IDs', () => {
|
||||
const provider = new BedrockModel({ modelId: 'unknown.model-v1:0' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'unknown.model-v1:0',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateConfig', () => {
|
||||
@@ -315,6 +359,7 @@ describe('BedrockModel', () => {
|
||||
modelId: 'global.anthropic.claude-sonnet-4-6',
|
||||
temperature: 0.8,
|
||||
maxTokens: 2048,
|
||||
contextWindowLimit: 1_000_000,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -332,6 +377,30 @@ describe('BedrockModel', () => {
|
||||
maxTokens: 1024,
|
||||
})
|
||||
})
|
||||
|
||||
it('re-resolves contextWindowLimit when modelId changes and it was auto-resolved', () => {
|
||||
const provider = new BedrockModel({ region: 'us-west-2' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_000_000)
|
||||
|
||||
provider.updateConfig({ modelId: 'anthropic.claude-haiku-4-5-20251001-v1:0' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(200_000)
|
||||
})
|
||||
|
||||
it('clears contextWindowLimit when modelId changes to unknown model', () => {
|
||||
const provider = new BedrockModel({ region: 'us-west-2' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_000_000)
|
||||
|
||||
provider.updateConfig({ modelId: 'my-custom-finetuned-model' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves explicit contextWindowLimit when modelId changes', () => {
|
||||
const provider = new BedrockModel({ region: 'us-west-2', contextWindowLimit: 50_000 })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000)
|
||||
|
||||
provider.updateConfig({ modelId: 'anthropic.claude-haiku-4-5-20251001-v1:0' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getConfig', () => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getContextWindowLimit } from '../defaults.js'
|
||||
|
||||
describe('getContextWindowLimit', () => {
|
||||
it('returns the context window limit for known model IDs across all providers', () => {
|
||||
// Anthropic direct API
|
||||
expect(getContextWindowLimit('claude-sonnet-4-6')).toBe(1_000_000)
|
||||
expect(getContextWindowLimit('claude-opus-4-6')).toBe(1_000_000)
|
||||
expect(getContextWindowLimit('claude-opus-4-5')).toBe(200_000)
|
||||
expect(getContextWindowLimit('claude-haiku-4-5')).toBe(200_000)
|
||||
// Bedrock Anthropic
|
||||
expect(getContextWindowLimit('anthropic.claude-sonnet-4-6')).toBe(1_000_000)
|
||||
// Bedrock Amazon Nova
|
||||
expect(getContextWindowLimit('amazon.nova-pro-v1:0')).toBe(300_000)
|
||||
expect(getContextWindowLimit('amazon.nova-micro-v1:0')).toBe(128_000)
|
||||
// OpenAI
|
||||
expect(getContextWindowLimit('gpt-5.4')).toBe(1_050_000)
|
||||
expect(getContextWindowLimit('gpt-4o')).toBe(128_000)
|
||||
expect(getContextWindowLimit('o3')).toBe(200_000)
|
||||
expect(getContextWindowLimit('o4-mini')).toBe(200_000)
|
||||
// Gemini
|
||||
expect(getContextWindowLimit('gemini-2.5-flash')).toBe(1_048_576)
|
||||
expect(getContextWindowLimit('gemini-2.5-pro')).toBe(1_048_576)
|
||||
})
|
||||
|
||||
it('strips Bedrock cross-region prefix before lookup', () => {
|
||||
expect(getContextWindowLimit('us.anthropic.claude-sonnet-4-6')).toBe(1_000_000)
|
||||
expect(getContextWindowLimit('global.anthropic.claude-sonnet-4-6')).toBe(1_000_000)
|
||||
})
|
||||
|
||||
it('does not strip unknown prefixes', () => {
|
||||
expect(getContextWindowLimit('custom.gpt-5.4')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for unknown model IDs', () => {
|
||||
expect(getContextWindowLimit('unknown-model-xyz')).toBeUndefined()
|
||||
expect(getContextWindowLimit('us.unknown.model-v1:0')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -139,8 +139,33 @@ describe('GoogleModel', () => {
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'gemini-2.5-flash',
|
||||
params: { temperature: 0.5 },
|
||||
contextWindowLimit: 1_048_576,
|
||||
})
|
||||
})
|
||||
|
||||
it('re-resolves contextWindowLimit when modelId changes and it was auto-resolved', () => {
|
||||
const provider = new GoogleModel({ apiKey: 'test-key' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_048_576)
|
||||
|
||||
provider.updateConfig({ modelId: 'gemini-2.0-flash' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_048_576)
|
||||
})
|
||||
|
||||
it('clears contextWindowLimit when modelId changes to unknown model', () => {
|
||||
const provider = new GoogleModel({ apiKey: 'test-key' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_048_576)
|
||||
|
||||
provider.updateConfig({ modelId: 'my-custom-finetuned-model' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves explicit contextWindowLimit when modelId changes', () => {
|
||||
const provider = new GoogleModel({ apiKey: 'test-key', contextWindowLimit: 50_000 })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000)
|
||||
|
||||
provider.updateConfig({ modelId: 'gemini-2.0-flash' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getConfig', () => {
|
||||
@@ -153,6 +178,7 @@ describe('GoogleModel', () => {
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'gemini-2.5-flash',
|
||||
params: { maxOutputTokens: 1024, temperature: 0.7 },
|
||||
contextWindowLimit: 1_048_576,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -167,6 +193,40 @@ describe('GoogleModel', () => {
|
||||
contextWindowLimit: 1_048_576,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit from model ID lookup', () => {
|
||||
const provider = new GoogleModel({ apiKey: 'test-key', modelId: 'gemini-2.5-pro' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'gemini-2.5-pro',
|
||||
contextWindowLimit: 1_048_576,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit for default model ID', () => {
|
||||
const provider = new GoogleModel({ apiKey: 'test-key' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
contextWindowLimit: 1_048_576,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not override explicit contextWindowLimit', () => {
|
||||
const provider = new GoogleModel({
|
||||
apiKey: 'test-key',
|
||||
modelId: 'gemini-2.5-flash',
|
||||
contextWindowLimit: 500_000,
|
||||
})
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'gemini-2.5-flash',
|
||||
contextWindowLimit: 500_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves contextWindowLimit undefined for unknown model IDs', () => {
|
||||
const provider = new GoogleModel({ apiKey: 'test-key', modelId: 'unknown-model' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'unknown-model',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('stream', () => {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import Anthropic, { type ClientOptions } from '@anthropic-ai/sdk'
|
||||
import { Model, type BaseModelConfig, type CountTokensOptions, type StreamOptions } from '../models/model.js'
|
||||
import {
|
||||
Model,
|
||||
type BaseModelConfig,
|
||||
type CountTokensOptions,
|
||||
type StreamOptions,
|
||||
resolveConfigMetadata,
|
||||
} from '../models/model.js'
|
||||
import type { Message, ContentBlock } from '../types/messages.js'
|
||||
import type { ModelStreamEvent } from '../models/streaming.js'
|
||||
import { createEmptyUsage } from '../models/streaming.js'
|
||||
@@ -81,7 +87,7 @@ export class AnthropicModel extends Model<AnthropicModelConfig> {
|
||||
}
|
||||
|
||||
getConfig(): AnthropicModelConfig {
|
||||
return this._config
|
||||
return resolveConfigMetadata(this._config, this._config.modelId ?? MODEL_DEFAULTS.anthropic.modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
type CountTokensOptions,
|
||||
Model,
|
||||
type StreamOptions,
|
||||
resolveConfigMetadata,
|
||||
} from '../models/model.js'
|
||||
import type { ContentBlock, Message, StopReason, ToolUseBlock } from '../types/messages.js'
|
||||
import type { ImageSource, VideoSource, DocumentSource } from '../types/media.js'
|
||||
@@ -463,7 +464,7 @@ export class BedrockModel extends Model<BedrockModelConfig> {
|
||||
* ```
|
||||
*/
|
||||
getConfig(): BedrockModelConfig {
|
||||
return this._config
|
||||
return resolveConfigMetadata(this._config, this._config.modelId ?? MODEL_DEFAULTS.bedrock.modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,3 +41,140 @@ export function defaultModelWarningMessage(defaultModelId: string): string {
|
||||
export function defaultMaxTokensWarningMessage(defaultMaxTokens: number): string {
|
||||
return `max_tokens=<${defaultMaxTokens}> | using default maxTokens, which is subject to change | set maxTokens explicitly to pin the value`
|
||||
}
|
||||
|
||||
/**
|
||||
* Context window limits (in tokens) for known model IDs.
|
||||
*
|
||||
* Best-effort lookup table — unknown models return `undefined` and callers
|
||||
* fall back gracefully (e.g. proactive compression is disabled).
|
||||
* Entries can be pruned when a model is no longer available from the provider.
|
||||
* Users can always override with an explicit `contextWindowLimit` in their model config.
|
||||
*
|
||||
* Values sourced from provider documentation and
|
||||
* https://github.com/BerriAI/litellm/blob/litellm_internal_staging/model_prices_and_context_window.json
|
||||
*
|
||||
* For Bedrock models with cross-region prefixes (e.g. `us.`, `eu.`, `global.`),
|
||||
* {@link getContextWindowLimit} strips the prefix before lookup so only the base model ID is needed here.
|
||||
*/
|
||||
const CONTEXT_WINDOW_LIMITS: Record<string, number> = {
|
||||
// Anthropic (direct API)
|
||||
'claude-sonnet-4-6': 1_000_000,
|
||||
'claude-sonnet-4-20250514': 1_000_000,
|
||||
'claude-sonnet-4-5': 200_000,
|
||||
'claude-sonnet-4-5-20250929': 200_000,
|
||||
'claude-opus-4-6': 1_000_000,
|
||||
'claude-opus-4-6-20260205': 1_000_000,
|
||||
'claude-opus-4-7': 1_000_000,
|
||||
'claude-opus-4-7-20260416': 1_000_000,
|
||||
'claude-opus-4-5': 200_000,
|
||||
'claude-opus-4-5-20251101': 200_000,
|
||||
'claude-opus-4-20250514': 200_000,
|
||||
'claude-opus-4-1': 200_000,
|
||||
'claude-opus-4-1-20250805': 200_000,
|
||||
'claude-haiku-4-5': 200_000,
|
||||
'claude-haiku-4-5-20251001': 200_000,
|
||||
'claude-3-7-sonnet-20250219': 200_000,
|
||||
'claude-3-5-sonnet-20241022': 200_000,
|
||||
'claude-3-5-sonnet-20240620': 200_000,
|
||||
'claude-3-5-haiku-20241022': 200_000,
|
||||
'claude-3-opus-20240229': 200_000,
|
||||
'claude-3-haiku-20240307': 200_000,
|
||||
|
||||
// Bedrock Anthropic (base model IDs — cross-region prefixes stripped by getContextWindowLimit)
|
||||
'anthropic.claude-sonnet-4-6': 1_000_000,
|
||||
'anthropic.claude-sonnet-4-20250514-v1:0': 1_000_000,
|
||||
'anthropic.claude-sonnet-4-5-20250929-v1:0': 200_000,
|
||||
'anthropic.claude-opus-4-6-v1': 1_000_000,
|
||||
'anthropic.claude-opus-4-7': 1_000_000,
|
||||
'anthropic.claude-opus-4-5-20251101-v1:0': 200_000,
|
||||
'anthropic.claude-opus-4-20250514-v1:0': 200_000,
|
||||
'anthropic.claude-opus-4-1-20250805-v1:0': 200_000,
|
||||
'anthropic.claude-haiku-4-5-20251001-v1:0': 200_000,
|
||||
'anthropic.claude-haiku-4-5@20251001': 200_000,
|
||||
'anthropic.claude-3-7-sonnet-20250219-v1:0': 200_000,
|
||||
'anthropic.claude-3-7-sonnet-20240620-v1:0': 200_000,
|
||||
'anthropic.claude-3-5-sonnet-20241022-v2:0': 200_000,
|
||||
'anthropic.claude-3-5-sonnet-20240620-v1:0': 200_000,
|
||||
'anthropic.claude-3-5-haiku-20241022-v1:0': 200_000,
|
||||
'anthropic.claude-3-opus-20240229-v1:0': 200_000,
|
||||
'anthropic.claude-3-haiku-20240307-v1:0': 200_000,
|
||||
'anthropic.claude-3-sonnet-20240229-v1:0': 200_000,
|
||||
'anthropic.claude-mythos-preview': 1_000_000,
|
||||
|
||||
// Bedrock Amazon Nova
|
||||
'amazon.nova-pro-v1:0': 300_000,
|
||||
'amazon.nova-lite-v1:0': 300_000,
|
||||
'amazon.nova-micro-v1:0': 128_000,
|
||||
'amazon.nova-premier-v1:0': 1_000_000,
|
||||
'amazon.nova-2-lite-v1:0': 1_000_000,
|
||||
'amazon.nova-2-pro-preview-20251202-v1:0': 1_000_000,
|
||||
|
||||
// OpenAI
|
||||
'gpt-5.5': 1_050_000,
|
||||
'gpt-5.5-pro': 1_050_000,
|
||||
'gpt-5.4': 1_050_000,
|
||||
'gpt-5.4-pro': 1_050_000,
|
||||
'gpt-5.4-mini': 272_000,
|
||||
'gpt-5.4-nano': 272_000,
|
||||
'gpt-5.2': 272_000,
|
||||
'gpt-5.2-pro': 272_000,
|
||||
'gpt-5.1': 272_000,
|
||||
'gpt-5': 272_000,
|
||||
'gpt-5-mini': 272_000,
|
||||
'gpt-5-nano': 272_000,
|
||||
'gpt-5-pro': 128_000,
|
||||
'gpt-4.1': 1_047_576,
|
||||
'gpt-4.1-mini': 1_047_576,
|
||||
'gpt-4.1-nano': 1_047_576,
|
||||
'gpt-4o': 128_000,
|
||||
'gpt-4o-mini': 128_000,
|
||||
'gpt-4-turbo': 128_000,
|
||||
o3: 200_000,
|
||||
'o3-mini': 200_000,
|
||||
'o3-pro': 200_000,
|
||||
'o4-mini': 200_000,
|
||||
o1: 200_000,
|
||||
|
||||
// Google Gemini
|
||||
'gemini-2.5-flash': 1_048_576,
|
||||
'gemini-2.5-flash-lite': 1_048_576,
|
||||
'gemini-2.5-pro': 1_048_576,
|
||||
'gemini-2.0-flash': 1_048_576,
|
||||
'gemini-2.0-flash-lite': 1_048_576,
|
||||
'gemini-3-pro-preview': 1_048_576,
|
||||
'gemini-3-flash-preview': 1_048_576,
|
||||
'gemini-3.1-pro-preview': 1_048_576,
|
||||
'gemini-3.1-flash-lite-preview': 1_048_576,
|
||||
}
|
||||
|
||||
/**
|
||||
* Known Bedrock cross-region routing prefixes.
|
||||
*
|
||||
* @see https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html
|
||||
*/
|
||||
const BEDROCK_REGION_PREFIXES = new Set(['us', 'eu', 'ap', 'global', 'apac', 'au', 'jp', 'us-gov'])
|
||||
|
||||
/**
|
||||
* Looks up the context window limit for a model ID.
|
||||
*
|
||||
* For Bedrock cross-region model IDs (e.g. `us.anthropic.claude-sonnet-4-6`),
|
||||
* the region prefix is stripped before lookup.
|
||||
*
|
||||
* @param modelId - The model ID to look up
|
||||
* @returns The context window limit in tokens, or undefined if not found
|
||||
*/
|
||||
export function getContextWindowLimit(modelId: string): number | undefined {
|
||||
const direct = CONTEXT_WINDOW_LIMITS[modelId]
|
||||
if (direct !== undefined) return direct
|
||||
|
||||
// Strip known Bedrock cross-region prefixes
|
||||
const dotIndex = modelId.indexOf('.')
|
||||
if (dotIndex !== -1) {
|
||||
const prefix = modelId.substring(0, dotIndex)
|
||||
if (BEDROCK_REGION_PREFIXES.has(prefix)) {
|
||||
return CONTEXT_WINDOW_LIMITS[modelId.substring(dotIndex + 1)]
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type GenerateContentConfig,
|
||||
type GenerateContentParameters,
|
||||
} from '@google/genai'
|
||||
import { Model } from '../model.js'
|
||||
import { Model, resolveConfigMetadata } from '../model.js'
|
||||
import type { CountTokensOptions, StreamOptions } from '../model.js'
|
||||
import type { Message } from '../../types/messages.js'
|
||||
import type { ModelStreamEvent } from '../streaming.js'
|
||||
@@ -144,7 +144,7 @@ export class GoogleModel extends Model<GoogleModelConfig> {
|
||||
* ```
|
||||
*/
|
||||
getConfig(): GoogleModelConfig {
|
||||
return this._config
|
||||
return resolveConfigMetadata(this._config, this._config.modelId ?? MODEL_DEFAULTS.gemini.modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,23 @@ import {
|
||||
import { MaxTokensError, ModelError, normalizeError } from '../errors.js'
|
||||
import type { Redaction } from '../hooks/events.js'
|
||||
import { logger } from '../logging/logger.js'
|
||||
import { getContextWindowLimit } from './defaults.js'
|
||||
|
||||
/**
|
||||
* Resolves model metadata fields on a config object from built-in lookup tables
|
||||
* when not explicitly set. Explicit values pass through unchanged.
|
||||
*
|
||||
* @internal
|
||||
* @param config - The stored model config
|
||||
* @param modelId - The model ID to look up
|
||||
* @returns A new config with resolved metadata, or the original config if nothing to resolve
|
||||
*/
|
||||
export function resolveConfigMetadata<T extends BaseModelConfig>(config: T, modelId: string): T {
|
||||
if (config.contextWindowLimit !== undefined) return config
|
||||
const limit = getContextWindowLimit(modelId)
|
||||
if (limit === undefined) return config
|
||||
return { ...config, contextWindowLimit: limit }
|
||||
}
|
||||
|
||||
class CitationAccumulator {
|
||||
citations: Citation[] = []
|
||||
@@ -98,6 +115,11 @@ export interface BaseModelConfig {
|
||||
* Maximum context window size in tokens for the model.
|
||||
*
|
||||
* This value represents the total token capacity shared between input and output.
|
||||
* When not provided, it is automatically resolved from a built-in lookup table
|
||||
* based on the configured model ID. An explicit value always takes precedence.
|
||||
*
|
||||
* When `modelId` is changed via `updateConfig()`, this value is automatically
|
||||
* re-resolved if it was initially auto-populated. Explicitly set values are preserved.
|
||||
*/
|
||||
contextWindowLimit?: number
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ describe('OpenAIModel', () => {
|
||||
modelId: 'gpt-5.4',
|
||||
temperature: 0.8,
|
||||
maxTokens: 2048,
|
||||
contextWindowLimit: 1_050_000,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -247,6 +248,22 @@ describe('OpenAIModel', () => {
|
||||
maxTokens: 1024,
|
||||
})
|
||||
})
|
||||
|
||||
it('re-resolves contextWindowLimit when modelId changes and it was auto-resolved', () => {
|
||||
const provider = new OpenAIModel({ api: 'chat', apiKey: 'sk-test' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(1_050_000) // gpt-5.4 default
|
||||
|
||||
provider.updateConfig({ modelId: 'gpt-4o' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(128_000) // gpt-4o value
|
||||
})
|
||||
|
||||
it('preserves explicit contextWindowLimit when modelId changes', () => {
|
||||
const provider = new OpenAIModel({ api: 'chat', apiKey: 'sk-test', contextWindowLimit: 50_000 })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000)
|
||||
|
||||
provider.updateConfig({ modelId: 'gpt-4o' })
|
||||
expect(provider.getConfig().contextWindowLimit).toBe(50_000) // preserved
|
||||
})
|
||||
})
|
||||
|
||||
describe('getConfig', () => {
|
||||
@@ -262,6 +279,7 @@ describe('OpenAIModel', () => {
|
||||
modelId: 'gpt-5.4',
|
||||
maxTokens: 1024,
|
||||
temperature: 0.7,
|
||||
contextWindowLimit: 1_050_000,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -277,6 +295,41 @@ describe('OpenAIModel', () => {
|
||||
contextWindowLimit: 128_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit from model ID lookup', () => {
|
||||
const provider = new OpenAIModel({ api: 'chat', modelId: 'gpt-4o', apiKey: 'sk-test' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'gpt-4o',
|
||||
contextWindowLimit: 128_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('auto-populates contextWindowLimit for default model ID', () => {
|
||||
const provider = new OpenAIModel({ api: 'chat', apiKey: 'sk-test' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
contextWindowLimit: 1_050_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not override explicit contextWindowLimit', () => {
|
||||
const provider = new OpenAIModel({
|
||||
api: 'chat',
|
||||
modelId: 'gpt-4o',
|
||||
apiKey: 'sk-test',
|
||||
contextWindowLimit: 50_000,
|
||||
})
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'gpt-4o',
|
||||
contextWindowLimit: 50_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves contextWindowLimit undefined for unknown model IDs', () => {
|
||||
const provider = new OpenAIModel({ api: 'chat', modelId: 'unknown-model', apiKey: 'sk-test' })
|
||||
expect(provider.getConfig()).toStrictEqual({
|
||||
modelId: 'unknown-model',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('managed params warning', () => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import OpenAI from 'openai'
|
||||
import type { ResponseStreamEvent } from 'openai/resources/responses/responses'
|
||||
import { Model } from '../model.js'
|
||||
import { Model, resolveConfigMetadata } from '../model.js'
|
||||
import type { StreamOptions } from '../model.js'
|
||||
import type { Message } from '../../types/messages.js'
|
||||
import type { ModelStreamEvent } from '../streaming.js'
|
||||
@@ -156,7 +156,7 @@ export class OpenAIModel extends Model<OpenAIModelConfig> {
|
||||
}
|
||||
|
||||
getConfig(): OpenAIModelConfig {
|
||||
return this._config
|
||||
return resolveConfigMetadata(this._config, this._config.modelId ?? MODEL_DEFAULTS.openai.modelId)
|
||||
}
|
||||
|
||||
async *stream(messages: Message[], options?: StreamOptions): AsyncIterable<ModelStreamEvent> {
|
||||
|
||||
Reference in New Issue
Block a user