|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | 2 | import type { Context, Model } from "../types.js"; |
3 | | -import { streamSimpleAnthropic } from "./anthropic.js"; |
| 3 | +import { isContextOverflow } from "../utils/overflow.js"; |
| 4 | +import { streamAnthropic, streamSimpleAnthropic } from "./anthropic.js"; |
| 5 | +import { clampMaxTokensToContext, estimateContextInputTokens } from "./simple-options.js"; |
4 | 6 |
|
5 | 7 | const apiKey = process.env.ANTHROPIC_API_KEY?.trim() ?? ""; |
6 | 8 | const live = process.env.OPENCLAW_LIVE_TEST === "1" && apiKey.length > 0; |
@@ -83,4 +85,66 @@ describeLive("Anthropic provider live", () => { |
83 | 85 | }, |
84 | 86 | timeoutMs, |
85 | 87 | ); |
| 88 | + |
| 89 | + it( |
| 90 | + "classifies the provider's current context-overflow error", |
| 91 | + async () => { |
| 92 | + const result = await streamAnthropic( |
| 93 | + model, |
| 94 | + { |
| 95 | + messages: [ |
| 96 | + { |
| 97 | + role: "user", |
| 98 | + content: "x ".repeat(Math.ceil(model.contextWindow * 1.1)), |
| 99 | + timestamp: 0, |
| 100 | + }, |
| 101 | + ], |
| 102 | + }, |
| 103 | + { apiKey, maxTokens: 1, maxRetries: 0 }, |
| 104 | + ).result(); |
| 105 | + |
| 106 | + expect(result.stopReason).toBe("error"); |
| 107 | + expect(isContextOverflow(result)).toBe(true); |
| 108 | + }, |
| 109 | + timeoutMs, |
| 110 | + ); |
| 111 | + |
| 112 | + it( |
| 113 | + "clamps an excessive output request to the remaining context", |
| 114 | + async () => { |
| 115 | + let sentMaxTokens: number | undefined; |
| 116 | + const contextWithoutSystem: Context = { |
| 117 | + messages: [{ role: "user", content: "Reply briefly with ok.", timestamp: 0 }], |
| 118 | + }; |
| 119 | + const promptUnit = "x "; |
| 120 | + const baseEstimate = estimateContextInputTokens(contextWithoutSystem); |
| 121 | + const unitEstimate = |
| 122 | + estimateContextInputTokens({ ...contextWithoutSystem, systemPrompt: promptUnit }) - |
| 123 | + baseEstimate; |
| 124 | + const targetEstimate = model.contextWindow - Math.floor(model.maxTokens / 2); |
| 125 | + const context: Context = { |
| 126 | + ...contextWithoutSystem, |
| 127 | + systemPrompt: promptUnit.repeat(Math.ceil((targetEstimate - baseEstimate) / unitEstimate)), |
| 128 | + }; |
| 129 | + const requestedMaxTokens = model.maxTokens * 100; |
| 130 | + expect(estimateContextInputTokens(context)).toBeGreaterThanOrEqual(targetEstimate); |
| 131 | + const expectedMaxTokens = clampMaxTokensToContext(model, context, requestedMaxTokens); |
| 132 | + expect(expectedMaxTokens).toBeLessThan(model.maxTokens); |
| 133 | + |
| 134 | + const result = await streamSimpleAnthropic(model, context, { |
| 135 | + apiKey, |
| 136 | + maxTokens: requestedMaxTokens, |
| 137 | + maxRetries: 0, |
| 138 | + reasoning: "off", |
| 139 | + onPayload: (payload) => { |
| 140 | + sentMaxTokens = (payload as { max_tokens?: number }).max_tokens; |
| 141 | + }, |
| 142 | + }).result(); |
| 143 | + |
| 144 | + expect(sentMaxTokens).toBe(expectedMaxTokens); |
| 145 | + expect(result.errorMessage).toBeUndefined(); |
| 146 | + expect(["stop", "length"]).toContain(result.stopReason); |
| 147 | + }, |
| 148 | + timeoutMs, |
| 149 | + ); |
86 | 150 | }); |
0 commit comments