From 2a20a95359489066942f2d04326df4b6035e48c2 Mon Sep 17 00:00:00 2001 From: Chay Nabors Date: Fri, 29 May 2026 16:43:15 -0400 Subject: [PATCH 1/2] Revert "fix: align context overflow detection patterns(#894) (#966)" This reverts commit a29f250d6a086ad4275cbed85fdab999c3bb0858. --- package-lock.json | 22 ++++++------------- .../src/models/__tests__/anthropic.test.ts | 11 ++-------- strands-ts/src/models/anthropic.ts | 12 ++-------- .../src/models/openai/__tests__/chat.test.ts | 12 ++-------- .../models/openai/__tests__/responses.test.ts | 22 ------------------- strands-ts/src/models/openai/errors.ts | 15 +++++-------- 6 files changed, 19 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45d44e6344..7d4b43a8c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2673,9 +2673,7 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "version": "2.0.4", "dev": true, "license": "BSD-3-Clause" }, @@ -2699,9 +2697,7 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", - "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "version": "1.1.0", "dev": true, "license": "BSD-3-Clause" }, @@ -2716,9 +2712,7 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "version": "1.1.0", "dev": true, "license": "BSD-3-Clause" }, @@ -7340,23 +7334,21 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz", - "integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==", + "version": "7.5.5", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", + "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.1", + "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", + "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" }, diff --git a/strands-ts/src/models/__tests__/anthropic.test.ts b/strands-ts/src/models/__tests__/anthropic.test.ts index f00ccf09bf..6457a6b23a 100644 --- a/strands-ts/src/models/__tests__/anthropic.test.ts +++ b/strands-ts/src/models/__tests__/anthropic.test.ts @@ -376,17 +376,10 @@ describe('AnthropicModel', () => { await expect(collectIterator(provider.stream(messages))).rejects.toThrow('API Error') }) - it.each([ - 'PROMPT IS TOO LONG: request exceeds context window', - 'max_tokens exceeded', - 'input too long', - 'input is too long', - 'input length exceeds context window', - 'input and output tokens exceed your context limit', - ])('maps context overflow error "%s" to ContextWindowOverflowError', async (message) => { + it('maps overload error to ContextWindowOverflowError', async () => { const mockClient = createMockClient(async function* () { yield { type: 'ping' } // Satisfy linter require-yield - throw new Error(message) + throw new Error('prompt is too long') }) const provider = new AnthropicModel({ client: mockClient }) const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })] diff --git a/strands-ts/src/models/anthropic.ts b/strands-ts/src/models/anthropic.ts index d44356ce2b..68285de6fe 100644 --- a/strands-ts/src/models/anthropic.ts +++ b/strands-ts/src/models/anthropic.ts @@ -16,14 +16,7 @@ import { logger } from '../logging/logger.js' import { warnOnce } from '../logging/warn-once.js' import { MODEL_DEFAULTS, defaultMaxTokensWarningMessage, defaultModelWarningMessage } from './defaults.js' -const CONTEXT_WINDOW_OVERFLOW_ERRORS = [ - 'prompt is too long', - 'max_tokens exceeded', - 'input too long', - 'input is too long', - 'input length exceeds context window', - 'input and output tokens exceed your context limit', -] +const CONTEXT_WINDOW_OVERFLOW_ERRORS = ['prompt is too long', 'max_tokens exceeded', 'input too long'] const TEXT_FILE_FORMATS = ['txt', 'md', 'markdown', 'csv', 'json', 'xml', 'html', 'yml', 'yaml', 'js', 'ts', 'py'] export interface AnthropicModelConfig extends BaseModelConfig { @@ -277,8 +270,7 @@ export class AnthropicModel extends Model { } catch (unknownError) { const error = normalizeError(unknownError) - const lowerMessage = error.message.toLowerCase() - if (CONTEXT_WINDOW_OVERFLOW_ERRORS.some((msg) => lowerMessage.includes(msg))) { + if (CONTEXT_WINDOW_OVERFLOW_ERRORS.some((msg) => error.message.includes(msg))) { throw new ContextWindowOverflowError(error.message) } diff --git a/strands-ts/src/models/openai/__tests__/chat.test.ts b/strands-ts/src/models/openai/__tests__/chat.test.ts index 5c1325a108..0601e36bd5 100644 --- a/strands-ts/src/models/openai/__tests__/chat.test.ts +++ b/strands-ts/src/models/openai/__tests__/chat.test.ts @@ -1643,20 +1643,12 @@ describe('OpenAIModel', () => { }).rejects.toThrow(ContextWindowOverflowError) }) - it.each([ - 'maximum context length exceeded', - 'context_length_exceeded', - 'too many tokens', - 'context length', - 'Input is too long for requested model', - 'input length and `max_tokens` exceed context limit', - 'too many total text bytes', - ])('throws ContextWindowOverflowError for error message pattern "%s"', async (message) => { + it('throws ContextWindowOverflowError for error with message pattern', async () => { const mockClient = { chat: { completions: { create: vi.fn(async () => { - throw new Error(message) + throw new Error('maximum context length exceeded') }), }, }, diff --git a/strands-ts/src/models/openai/__tests__/responses.test.ts b/strands-ts/src/models/openai/__tests__/responses.test.ts index bfceee935f..fec374a5b0 100644 --- a/strands-ts/src/models/openai/__tests__/responses.test.ts +++ b/strands-ts/src/models/openai/__tests__/responses.test.ts @@ -719,28 +719,6 @@ describe("OpenAIModel (api: 'responses')", () => { ).rejects.toBeInstanceOf(ContextWindowOverflowError) }) - it.each([ - 'maximum context length exceeded', - 'context_length_exceeded', - 'too many tokens', - 'context length', - 'Input is too long for requested model', - 'input length and `max_tokens` exceed context limit', - 'too many total text bytes', - ])('wraps context overflow message pattern "%s" as ContextWindowOverflowError', async (message) => { - const client: any = { - responses: { - create: vi.fn(async () => { - throw new Error(message) - }), - }, - } - const model = new OpenAIModel({ api: 'responses', client }) - await expect( - collectIterator(model.stream([new Message({ role: 'user', content: [new TextBlock('x')] })])) - ).rejects.toBeInstanceOf(ContextWindowOverflowError) - }) - it('rethrows unknown errors untouched', async () => { const client: any = { responses: { diff --git a/strands-ts/src/models/openai/errors.ts b/strands-ts/src/models/openai/errors.ts index 7bc0af5090..73cf1c3375 100644 --- a/strands-ts/src/models/openai/errors.ts +++ b/strands-ts/src/models/openai/errors.ts @@ -14,9 +14,6 @@ const CONTEXT_WINDOW_OVERFLOW_PATTERNS = [ 'context_length_exceeded', 'too many tokens', 'context length', - 'Input is too long for requested model', - 'input length and `max_tokens` exceed context limit', - 'too many total text bytes', ] /** @@ -35,16 +32,16 @@ export type OpenAIErrorKind = 'contextOverflow' | 'throttling' */ export function classifyOpenAIError(err: Error & { status?: number; code?: string }): OpenAIErrorKind | undefined { const message = err.message?.toLowerCase() ?? '' - const code = err.code?.toLowerCase() ?? '' - if (err.status === 429 || code === 'rate_limit_exceeded' || RATE_LIMIT_PATTERNS.some((p) => message.includes(p))) { + if ( + err.status === 429 || + err.code === 'rate_limit_exceeded' || + RATE_LIMIT_PATTERNS.some((p) => message.includes(p)) + ) { return 'throttling' } - if ( - code === 'context_length_exceeded' || - CONTEXT_WINDOW_OVERFLOW_PATTERNS.some((pattern) => message.includes(pattern.toLowerCase())) - ) { + if (err.code === 'context_length_exceeded' || CONTEXT_WINDOW_OVERFLOW_PATTERNS.some((p) => message.includes(p))) { return 'contextOverflow' } From 868a8a1a6d1d46f3e60bca65c0138d7a82328180 Mon Sep 17 00:00:00 2001 From: Chay Nabors Date: Fri, 29 May 2026 16:57:03 -0400 Subject: [PATCH 2/2] fix: realign provider context-overflow patterns and drop MIT dual-license --- LICENSE.MIT | 21 -------- package-lock.json | 53 +++++++++++++------ strands-py-wasm/pyproject.toml | 4 +- .../src/models/__tests__/anthropic.test.ts | 26 +++++++++ strands-ts/src/models/anthropic.ts | 15 +++++- .../models/openai/__tests__/errors.test.ts | 34 ++++++++++++ strands-ts/src/models/openai/errors.ts | 15 +++--- 7 files changed, 120 insertions(+), 48 deletions(-) delete mode 100644 LICENSE.MIT create mode 100644 strands-ts/src/models/openai/__tests__/errors.test.ts diff --git a/LICENSE.MIT b/LICENSE.MIT deleted file mode 100644 index 5d2ae23ae8..0000000000 --- a/LICENSE.MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Strands Agents Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/package-lock.json b/package-lock.json index 7d4b43a8c4..ab84750f07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2664,6 +2664,8 @@ }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "dev": true, "license": "BSD-3-Clause" }, @@ -2673,22 +2675,27 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -2697,7 +2704,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.0", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", "dev": true, "license": "BSD-3-Clause" }, @@ -2712,7 +2721,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "dev": true, "license": "BSD-3-Clause" }, @@ -4435,7 +4446,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.5", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -7334,23 +7347,25 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.5", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", + "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -7376,7 +7391,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -8541,7 +8558,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.0", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { diff --git a/strands-py-wasm/pyproject.toml b/strands-py-wasm/pyproject.toml index 52717c832d..cd4d147641 100644 --- a/strands-py-wasm/pyproject.toml +++ b/strands-py-wasm/pyproject.toml @@ -8,8 +8,8 @@ name = "strands-agents" version = "2.0.0a1" description = "A model-driven approach to building AI agents in just a few lines of code" requires-python = ">=3.10" -license = "Apache-2.0 OR MIT" -license-files = ["LICENSE.APACHE", "LICENSE.MIT"] +license = "Apache-2.0" +license-files = ["LICENSE.APACHE"] authors = [ {name = "AWS", email = "opensource@amazon.com"}, ] diff --git a/strands-ts/src/models/__tests__/anthropic.test.ts b/strands-ts/src/models/__tests__/anthropic.test.ts index 6457a6b23a..9f46eaedbb 100644 --- a/strands-ts/src/models/__tests__/anthropic.test.ts +++ b/strands-ts/src/models/__tests__/anthropic.test.ts @@ -387,6 +387,32 @@ describe('AnthropicModel', () => { await expect(collectIterator(provider.stream(messages))).rejects.toThrow(ContextWindowOverflowError) }) + it.each([ + 'input is too long', + 'input length exceeds context window', + 'input and output tokens exceed your context limit', + ])('maps overflow phrase %p to ContextWindowOverflowError', async (phrase) => { + const mockClient = createMockClient(async function* () { + yield { type: 'ping' } + throw new Error(phrase) + }) + const provider = new AnthropicModel({ client: mockClient }) + const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })] + + await expect(collectIterator(provider.stream(messages))).rejects.toThrow(ContextWindowOverflowError) + }) + + it('matches overflow phrases case-insensitively', async () => { + const mockClient = createMockClient(async function* () { + yield { type: 'ping' } + throw new Error('PROMPT IS TOO LONG: 200000 tokens') + }) + const provider = new AnthropicModel({ client: mockClient }) + const messages = [new Message({ role: 'user', content: [new TextBlock('Hi')] })] + + await expect(collectIterator(provider.stream(messages))).rejects.toThrow(ContextWindowOverflowError) + }) + it('maps HTTP 429 error to ModelThrottledError', async () => { const rateLimitError = Object.assign(new Error('Rate limit exceeded'), { status: 429 }) // eslint-disable-next-line require-yield diff --git a/strands-ts/src/models/anthropic.ts b/strands-ts/src/models/anthropic.ts index 68285de6fe..c68191fcdf 100644 --- a/strands-ts/src/models/anthropic.ts +++ b/strands-ts/src/models/anthropic.ts @@ -16,7 +16,17 @@ import { logger } from '../logging/logger.js' import { warnOnce } from '../logging/warn-once.js' import { MODEL_DEFAULTS, defaultMaxTokensWarningMessage, defaultModelWarningMessage } from './defaults.js' -const CONTEXT_WINDOW_OVERFLOW_ERRORS = ['prompt is too long', 'max_tokens exceeded', 'input too long'] +// Union of overflow phrases observed across Anthropic responses, matched +// case-insensitively. Kept in lowercase so the comparison is a single +// ``toLowerCase`` on the error message. +const CONTEXT_WINDOW_OVERFLOW_ERRORS = [ + 'prompt is too long', + 'max_tokens exceeded', + 'input too long', + 'input is too long', + 'input length exceeds context window', + 'input and output tokens exceed your context limit', +] const TEXT_FILE_FORMATS = ['txt', 'md', 'markdown', 'csv', 'json', 'xml', 'html', 'yml', 'yaml', 'js', 'ts', 'py'] export interface AnthropicModelConfig extends BaseModelConfig { @@ -270,7 +280,8 @@ export class AnthropicModel extends Model { } catch (unknownError) { const error = normalizeError(unknownError) - if (CONTEXT_WINDOW_OVERFLOW_ERRORS.some((msg) => error.message.includes(msg))) { + const lowered = error.message.toLowerCase() + if (CONTEXT_WINDOW_OVERFLOW_ERRORS.some((msg) => lowered.includes(msg))) { throw new ContextWindowOverflowError(error.message) } diff --git a/strands-ts/src/models/openai/__tests__/errors.test.ts b/strands-ts/src/models/openai/__tests__/errors.test.ts new file mode 100644 index 0000000000..708022fc5c --- /dev/null +++ b/strands-ts/src/models/openai/__tests__/errors.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest' +import { classifyOpenAIError } from '../errors.js' + +describe('classifyOpenAIError', () => { + it.each([ + 'maximum context length exceeded', + 'context_length_exceeded', + 'too many tokens', + 'context length', + 'Input is too long for requested model', + 'input length and `max_tokens` exceed context limit', + 'too many total text bytes', + ])('classifies overflow phrase %p as contextOverflow', (phrase) => { + expect(classifyOpenAIError(new Error(phrase))).toBe('contextOverflow') + }) + + it('matches overflow phrases case-insensitively', () => { + expect(classifyOpenAIError(new Error('MAXIMUM CONTEXT LENGTH EXCEEDED'))).toBe('contextOverflow') + }) + + it('classifies a structured context_length_exceeded code as contextOverflow', () => { + const err = Object.assign(new Error('something opaque'), { code: 'context_length_exceeded' }) + expect(classifyOpenAIError(err)).toBe('contextOverflow') + }) + + it('matches structured codes case-insensitively', () => { + const err = Object.assign(new Error('something opaque'), { code: 'Context_Length_Exceeded' }) + expect(classifyOpenAIError(err)).toBe('contextOverflow') + }) + + it('returns undefined for unrelated errors', () => { + expect(classifyOpenAIError(new Error('a totally unrelated failure'))).toBeUndefined() + }) +}) diff --git a/strands-ts/src/models/openai/errors.ts b/strands-ts/src/models/openai/errors.ts index 73cf1c3375..b4aff9a723 100644 --- a/strands-ts/src/models/openai/errors.ts +++ b/strands-ts/src/models/openai/errors.ts @@ -9,11 +9,17 @@ * * @see https://platform.openai.com/docs/guides/error-codes */ +// Union of overflow phrases observed across OpenAI responses, matched +// case-insensitively. Lowercased here so the comparison is a single +// ``toLowerCase`` on the error message. const CONTEXT_WINDOW_OVERFLOW_PATTERNS = [ 'maximum context length', 'context_length_exceeded', 'too many tokens', 'context length', + 'input is too long for requested model', + 'input length and `max_tokens` exceed context limit', + 'too many total text bytes', ] /** @@ -32,16 +38,13 @@ export type OpenAIErrorKind = 'contextOverflow' | 'throttling' */ export function classifyOpenAIError(err: Error & { status?: number; code?: string }): OpenAIErrorKind | undefined { const message = err.message?.toLowerCase() ?? '' + const code = err.code?.toLowerCase() ?? '' - if ( - err.status === 429 || - err.code === 'rate_limit_exceeded' || - RATE_LIMIT_PATTERNS.some((p) => message.includes(p)) - ) { + if (err.status === 429 || code === 'rate_limit_exceeded' || RATE_LIMIT_PATTERNS.some((p) => message.includes(p))) { return 'throttling' } - if (err.code === 'context_length_exceeded' || CONTEXT_WINDOW_OVERFLOW_PATTERNS.some((p) => message.includes(p))) { + if (code === 'context_length_exceeded' || CONTEXT_WINDOW_OVERFLOW_PATTERNS.some((p) => message.includes(p))) { return 'contextOverflow' }