@dotsetlabs/bellwether 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +291 -0
- package/LICENSE +21 -0
- package/README.md +739 -0
- package/dist/auth/credentials.d.ts +64 -0
- package/dist/auth/credentials.js +218 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/keychain.d.ts +64 -0
- package/dist/auth/keychain.js +268 -0
- package/dist/baseline/ab-testing.d.ts +80 -0
- package/dist/baseline/ab-testing.js +236 -0
- package/dist/baseline/ai-compatibility-scorer.d.ts +95 -0
- package/dist/baseline/ai-compatibility-scorer.js +606 -0
- package/dist/baseline/calibration.d.ts +77 -0
- package/dist/baseline/calibration.js +136 -0
- package/dist/baseline/category-matching.d.ts +85 -0
- package/dist/baseline/category-matching.js +289 -0
- package/dist/baseline/change-impact-analyzer.d.ts +98 -0
- package/dist/baseline/change-impact-analyzer.js +592 -0
- package/dist/baseline/comparator.d.ts +64 -0
- package/dist/baseline/comparator.js +916 -0
- package/dist/baseline/confidence.d.ts +55 -0
- package/dist/baseline/confidence.js +122 -0
- package/dist/baseline/converter.d.ts +61 -0
- package/dist/baseline/converter.js +585 -0
- package/dist/baseline/dependency-analyzer.d.ts +89 -0
- package/dist/baseline/dependency-analyzer.js +567 -0
- package/dist/baseline/deprecation-tracker.d.ts +133 -0
- package/dist/baseline/deprecation-tracker.js +322 -0
- package/dist/baseline/diff.d.ts +55 -0
- package/dist/baseline/diff.js +1584 -0
- package/dist/baseline/documentation-scorer.d.ts +205 -0
- package/dist/baseline/documentation-scorer.js +466 -0
- package/dist/baseline/embeddings.d.ts +118 -0
- package/dist/baseline/embeddings.js +251 -0
- package/dist/baseline/error-analyzer.d.ts +198 -0
- package/dist/baseline/error-analyzer.js +721 -0
- package/dist/baseline/evaluation/evaluator.d.ts +42 -0
- package/dist/baseline/evaluation/evaluator.js +323 -0
- package/dist/baseline/evaluation/expanded-dataset.d.ts +45 -0
- package/dist/baseline/evaluation/expanded-dataset.js +1164 -0
- package/dist/baseline/evaluation/golden-dataset.d.ts +58 -0
- package/dist/baseline/evaluation/golden-dataset.js +717 -0
- package/dist/baseline/evaluation/index.d.ts +15 -0
- package/dist/baseline/evaluation/index.js +15 -0
- package/dist/baseline/evaluation/types.d.ts +186 -0
- package/dist/baseline/evaluation/types.js +8 -0
- package/dist/baseline/external-dependency-detector.d.ts +181 -0
- package/dist/baseline/external-dependency-detector.js +524 -0
- package/dist/baseline/golden-output.d.ts +162 -0
- package/dist/baseline/golden-output.js +636 -0
- package/dist/baseline/health-scorer.d.ts +174 -0
- package/dist/baseline/health-scorer.js +451 -0
- package/dist/baseline/incremental-checker.d.ts +97 -0
- package/dist/baseline/incremental-checker.js +174 -0
- package/dist/baseline/index.d.ts +31 -0
- package/dist/baseline/index.js +42 -0
- package/dist/baseline/migration-generator.d.ts +137 -0
- package/dist/baseline/migration-generator.js +554 -0
- package/dist/baseline/migrations.d.ts +60 -0
- package/dist/baseline/migrations.js +197 -0
- package/dist/baseline/performance-tracker.d.ts +214 -0
- package/dist/baseline/performance-tracker.js +577 -0
- package/dist/baseline/pr-comment-generator.d.ts +117 -0
- package/dist/baseline/pr-comment-generator.js +546 -0
- package/dist/baseline/response-fingerprint.d.ts +127 -0
- package/dist/baseline/response-fingerprint.js +728 -0
- package/dist/baseline/response-schema-tracker.d.ts +129 -0
- package/dist/baseline/response-schema-tracker.js +420 -0
- package/dist/baseline/risk-scorer.d.ts +54 -0
- package/dist/baseline/risk-scorer.js +434 -0
- package/dist/baseline/saver.d.ts +89 -0
- package/dist/baseline/saver.js +554 -0
- package/dist/baseline/scenario-generator.d.ts +151 -0
- package/dist/baseline/scenario-generator.js +905 -0
- package/dist/baseline/schema-compare.d.ts +86 -0
- package/dist/baseline/schema-compare.js +557 -0
- package/dist/baseline/schema-evolution.d.ts +189 -0
- package/dist/baseline/schema-evolution.js +467 -0
- package/dist/baseline/semantic.d.ts +203 -0
- package/dist/baseline/semantic.js +908 -0
- package/dist/baseline/synonyms.d.ts +60 -0
- package/dist/baseline/synonyms.js +386 -0
- package/dist/baseline/telemetry.d.ts +165 -0
- package/dist/baseline/telemetry.js +294 -0
- package/dist/baseline/test-pruner.d.ts +120 -0
- package/dist/baseline/test-pruner.js +387 -0
- package/dist/baseline/types.d.ts +449 -0
- package/dist/baseline/types.js +5 -0
- package/dist/baseline/version.d.ts +138 -0
- package/dist/baseline/version.js +206 -0
- package/dist/cache/index.d.ts +5 -0
- package/dist/cache/index.js +5 -0
- package/dist/cache/response-cache.d.ts +151 -0
- package/dist/cache/response-cache.js +287 -0
- package/dist/ci/index.d.ts +60 -0
- package/dist/ci/index.js +342 -0
- package/dist/cli/commands/auth.d.ts +12 -0
- package/dist/cli/commands/auth.js +352 -0
- package/dist/cli/commands/badge.d.ts +3 -0
- package/dist/cli/commands/badge.js +74 -0
- package/dist/cli/commands/baseline-accept.d.ts +15 -0
- package/dist/cli/commands/baseline-accept.js +178 -0
- package/dist/cli/commands/baseline-migrate.d.ts +12 -0
- package/dist/cli/commands/baseline-migrate.js +164 -0
- package/dist/cli/commands/baseline.d.ts +14 -0
- package/dist/cli/commands/baseline.js +449 -0
- package/dist/cli/commands/beta.d.ts +10 -0
- package/dist/cli/commands/beta.js +231 -0
- package/dist/cli/commands/check.d.ts +11 -0
- package/dist/cli/commands/check.js +820 -0
- package/dist/cli/commands/cloud/badge.d.ts +3 -0
- package/dist/cli/commands/cloud/badge.js +74 -0
- package/dist/cli/commands/cloud/diff.d.ts +6 -0
- package/dist/cli/commands/cloud/diff.js +79 -0
- package/dist/cli/commands/cloud/history.d.ts +6 -0
- package/dist/cli/commands/cloud/history.js +102 -0
- package/dist/cli/commands/cloud/link.d.ts +9 -0
- package/dist/cli/commands/cloud/link.js +119 -0
- package/dist/cli/commands/cloud/login.d.ts +7 -0
- package/dist/cli/commands/cloud/login.js +499 -0
- package/dist/cli/commands/cloud/projects.d.ts +6 -0
- package/dist/cli/commands/cloud/projects.js +44 -0
- package/dist/cli/commands/cloud/shared.d.ts +7 -0
- package/dist/cli/commands/cloud/shared.js +42 -0
- package/dist/cli/commands/cloud/teams.d.ts +8 -0
- package/dist/cli/commands/cloud/teams.js +169 -0
- package/dist/cli/commands/cloud/upload.d.ts +8 -0
- package/dist/cli/commands/cloud/upload.js +181 -0
- package/dist/cli/commands/contract.d.ts +11 -0
- package/dist/cli/commands/contract.js +280 -0
- package/dist/cli/commands/discover.d.ts +3 -0
- package/dist/cli/commands/discover.js +82 -0
- package/dist/cli/commands/eval.d.ts +9 -0
- package/dist/cli/commands/eval.js +187 -0
- package/dist/cli/commands/explore.d.ts +11 -0
- package/dist/cli/commands/explore.js +437 -0
- package/dist/cli/commands/feedback.d.ts +9 -0
- package/dist/cli/commands/feedback.js +174 -0
- package/dist/cli/commands/golden.d.ts +12 -0
- package/dist/cli/commands/golden.js +407 -0
- package/dist/cli/commands/history.d.ts +10 -0
- package/dist/cli/commands/history.js +202 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +219 -0
- package/dist/cli/commands/interview.d.ts +3 -0
- package/dist/cli/commands/interview.js +903 -0
- package/dist/cli/commands/link.d.ts +10 -0
- package/dist/cli/commands/link.js +169 -0
- package/dist/cli/commands/login.d.ts +7 -0
- package/dist/cli/commands/login.js +499 -0
- package/dist/cli/commands/preset.d.ts +33 -0
- package/dist/cli/commands/preset.js +297 -0
- package/dist/cli/commands/profile.d.ts +33 -0
- package/dist/cli/commands/profile.js +286 -0
- package/dist/cli/commands/registry.d.ts +11 -0
- package/dist/cli/commands/registry.js +146 -0
- package/dist/cli/commands/shared.d.ts +79 -0
- package/dist/cli/commands/shared.js +196 -0
- package/dist/cli/commands/teams.d.ts +8 -0
- package/dist/cli/commands/teams.js +169 -0
- package/dist/cli/commands/test.d.ts +9 -0
- package/dist/cli/commands/test.js +500 -0
- package/dist/cli/commands/upload.d.ts +8 -0
- package/dist/cli/commands/upload.js +223 -0
- package/dist/cli/commands/validate-config.d.ts +6 -0
- package/dist/cli/commands/validate-config.js +35 -0
- package/dist/cli/commands/verify.d.ts +11 -0
- package/dist/cli/commands/verify.js +283 -0
- package/dist/cli/commands/watch.d.ts +12 -0
- package/dist/cli/commands/watch.js +253 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +178 -0
- package/dist/cli/interactive.d.ts +47 -0
- package/dist/cli/interactive.js +216 -0
- package/dist/cli/output/terminal-reporter.d.ts +19 -0
- package/dist/cli/output/terminal-reporter.js +104 -0
- package/dist/cli/output.d.ts +226 -0
- package/dist/cli/output.js +438 -0
- package/dist/cli/utils/env.d.ts +5 -0
- package/dist/cli/utils/env.js +14 -0
- package/dist/cli/utils/progress.d.ts +59 -0
- package/dist/cli/utils/progress.js +206 -0
- package/dist/cli/utils/server-context.d.ts +10 -0
- package/dist/cli/utils/server-context.js +36 -0
- package/dist/cloud/auth.d.ts +144 -0
- package/dist/cloud/auth.js +374 -0
- package/dist/cloud/client.d.ts +24 -0
- package/dist/cloud/client.js +65 -0
- package/dist/cloud/http-client.d.ts +38 -0
- package/dist/cloud/http-client.js +215 -0
- package/dist/cloud/index.d.ts +23 -0
- package/dist/cloud/index.js +25 -0
- package/dist/cloud/mock-client.d.ts +107 -0
- package/dist/cloud/mock-client.js +545 -0
- package/dist/cloud/types.d.ts +515 -0
- package/dist/cloud/types.js +15 -0
- package/dist/config/defaults.d.ts +160 -0
- package/dist/config/defaults.js +169 -0
- package/dist/config/loader.d.ts +24 -0
- package/dist/config/loader.js +122 -0
- package/dist/config/template.d.ts +42 -0
- package/dist/config/template.js +647 -0
- package/dist/config/validator.d.ts +2112 -0
- package/dist/config/validator.js +658 -0
- package/dist/constants/cloud.d.ts +107 -0
- package/dist/constants/cloud.js +110 -0
- package/dist/constants/core.d.ts +521 -0
- package/dist/constants/core.js +556 -0
- package/dist/constants/testing.d.ts +1283 -0
- package/dist/constants/testing.js +1568 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +10 -0
- package/dist/contract/index.d.ts +6 -0
- package/dist/contract/index.js +5 -0
- package/dist/contract/validator.d.ts +177 -0
- package/dist/contract/validator.js +574 -0
- package/dist/cost/index.d.ts +6 -0
- package/dist/cost/index.js +5 -0
- package/dist/cost/tracker.d.ts +134 -0
- package/dist/cost/tracker.js +313 -0
- package/dist/discovery/discovery.d.ts +16 -0
- package/dist/discovery/discovery.js +173 -0
- package/dist/discovery/types.d.ts +51 -0
- package/dist/discovery/types.js +2 -0
- package/dist/docs/agents.d.ts +3 -0
- package/dist/docs/agents.js +995 -0
- package/dist/docs/contract.d.ts +51 -0
- package/dist/docs/contract.js +1681 -0
- package/dist/docs/generator.d.ts +4 -0
- package/dist/docs/generator.js +4 -0
- package/dist/docs/html-reporter.d.ts +9 -0
- package/dist/docs/html-reporter.js +757 -0
- package/dist/docs/index.d.ts +10 -0
- package/dist/docs/index.js +11 -0
- package/dist/docs/junit-reporter.d.ts +18 -0
- package/dist/docs/junit-reporter.js +210 -0
- package/dist/docs/report.d.ts +14 -0
- package/dist/docs/report.js +44 -0
- package/dist/docs/sarif-reporter.d.ts +19 -0
- package/dist/docs/sarif-reporter.js +335 -0
- package/dist/docs/shared.d.ts +35 -0
- package/dist/docs/shared.js +162 -0
- package/dist/docs/templates.d.ts +12 -0
- package/dist/docs/templates.js +76 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.js +6 -0
- package/dist/errors/retry.d.ts +92 -0
- package/dist/errors/retry.js +323 -0
- package/dist/errors/types.d.ts +321 -0
- package/dist/errors/types.js +584 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +32 -0
- package/dist/interview/dependency-resolver.d.ts +11 -0
- package/dist/interview/dependency-resolver.js +32 -0
- package/dist/interview/interviewer.d.ts +232 -0
- package/dist/interview/interviewer.js +1939 -0
- package/dist/interview/mock-response-generator.d.ts +7 -0
- package/dist/interview/mock-response-generator.js +102 -0
- package/dist/interview/orchestrator.d.ts +237 -0
- package/dist/interview/orchestrator.js +1296 -0
- package/dist/interview/rate-limiter.d.ts +15 -0
- package/dist/interview/rate-limiter.js +55 -0
- package/dist/interview/response-validator.d.ts +10 -0
- package/dist/interview/response-validator.js +132 -0
- package/dist/interview/schema-inferrer.d.ts +8 -0
- package/dist/interview/schema-inferrer.js +71 -0
- package/dist/interview/schema-test-generator.d.ts +71 -0
- package/dist/interview/schema-test-generator.js +834 -0
- package/dist/interview/smart-value-generator.d.ts +155 -0
- package/dist/interview/smart-value-generator.js +554 -0
- package/dist/interview/stateful-test-runner.d.ts +19 -0
- package/dist/interview/stateful-test-runner.js +106 -0
- package/dist/interview/types.d.ts +561 -0
- package/dist/interview/types.js +2 -0
- package/dist/llm/anthropic.d.ts +41 -0
- package/dist/llm/anthropic.js +355 -0
- package/dist/llm/client.d.ts +123 -0
- package/dist/llm/client.js +42 -0
- package/dist/llm/factory.d.ts +38 -0
- package/dist/llm/factory.js +145 -0
- package/dist/llm/fallback.d.ts +140 -0
- package/dist/llm/fallback.js +379 -0
- package/dist/llm/index.d.ts +18 -0
- package/dist/llm/index.js +15 -0
- package/dist/llm/ollama.d.ts +37 -0
- package/dist/llm/ollama.js +330 -0
- package/dist/llm/openai.d.ts +25 -0
- package/dist/llm/openai.js +320 -0
- package/dist/llm/token-budget.d.ts +161 -0
- package/dist/llm/token-budget.js +395 -0
- package/dist/logging/logger.d.ts +70 -0
- package/dist/logging/logger.js +130 -0
- package/dist/metrics/collector.d.ts +106 -0
- package/dist/metrics/collector.js +547 -0
- package/dist/metrics/index.d.ts +7 -0
- package/dist/metrics/index.js +7 -0
- package/dist/metrics/prometheus.d.ts +20 -0
- package/dist/metrics/prometheus.js +241 -0
- package/dist/metrics/types.d.ts +209 -0
- package/dist/metrics/types.js +5 -0
- package/dist/persona/builtins.d.ts +54 -0
- package/dist/persona/builtins.js +219 -0
- package/dist/persona/index.d.ts +8 -0
- package/dist/persona/index.js +8 -0
- package/dist/persona/loader.d.ts +30 -0
- package/dist/persona/loader.js +190 -0
- package/dist/persona/types.d.ts +144 -0
- package/dist/persona/types.js +5 -0
- package/dist/persona/validation.d.ts +94 -0
- package/dist/persona/validation.js +332 -0
- package/dist/prompts/index.d.ts +5 -0
- package/dist/prompts/index.js +5 -0
- package/dist/prompts/templates.d.ts +180 -0
- package/dist/prompts/templates.js +431 -0
- package/dist/registry/client.d.ts +49 -0
- package/dist/registry/client.js +191 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.js +6 -0
- package/dist/registry/types.d.ts +140 -0
- package/dist/registry/types.js +6 -0
- package/dist/scenarios/evaluator.d.ts +43 -0
- package/dist/scenarios/evaluator.js +206 -0
- package/dist/scenarios/index.d.ts +10 -0
- package/dist/scenarios/index.js +9 -0
- package/dist/scenarios/loader.d.ts +20 -0
- package/dist/scenarios/loader.js +285 -0
- package/dist/scenarios/types.d.ts +153 -0
- package/dist/scenarios/types.js +8 -0
- package/dist/security/index.d.ts +17 -0
- package/dist/security/index.js +18 -0
- package/dist/security/payloads.d.ts +61 -0
- package/dist/security/payloads.js +268 -0
- package/dist/security/security-tester.d.ts +42 -0
- package/dist/security/security-tester.js +582 -0
- package/dist/security/types.d.ts +166 -0
- package/dist/security/types.js +8 -0
- package/dist/transport/base-transport.d.ts +59 -0
- package/dist/transport/base-transport.js +38 -0
- package/dist/transport/http-transport.d.ts +67 -0
- package/dist/transport/http-transport.js +238 -0
- package/dist/transport/mcp-client.d.ts +141 -0
- package/dist/transport/mcp-client.js +496 -0
- package/dist/transport/sse-transport.d.ts +88 -0
- package/dist/transport/sse-transport.js +316 -0
- package/dist/transport/stdio-transport.d.ts +43 -0
- package/dist/transport/stdio-transport.js +238 -0
- package/dist/transport/types.d.ts +125 -0
- package/dist/transport/types.js +16 -0
- package/dist/utils/concurrency.d.ts +123 -0
- package/dist/utils/concurrency.js +213 -0
- package/dist/utils/formatters.d.ts +16 -0
- package/dist/utils/formatters.js +37 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/jsonpath.d.ts +87 -0
- package/dist/utils/jsonpath.js +326 -0
- package/dist/utils/markdown.d.ts +113 -0
- package/dist/utils/markdown.js +265 -0
- package/dist/utils/network.d.ts +14 -0
- package/dist/utils/network.js +17 -0
- package/dist/utils/sanitize.d.ts +92 -0
- package/dist/utils/sanitize.js +191 -0
- package/dist/utils/semantic.d.ts +194 -0
- package/dist/utils/semantic.js +1051 -0
- package/dist/utils/smart-truncate.d.ts +94 -0
- package/dist/utils/smart-truncate.js +361 -0
- package/dist/utils/timeout.d.ts +153 -0
- package/dist/utils/timeout.js +205 -0
- package/dist/utils/yaml-parser.d.ts +58 -0
- package/dist/utils/yaml-parser.js +86 -0
- package/dist/validation/index.d.ts +32 -0
- package/dist/validation/index.js +32 -0
- package/dist/validation/semantic-test-generator.d.ts +50 -0
- package/dist/validation/semantic-test-generator.js +176 -0
- package/dist/validation/semantic-types.d.ts +66 -0
- package/dist/validation/semantic-types.js +94 -0
- package/dist/validation/semantic-validator.d.ts +38 -0
- package/dist/validation/semantic-validator.js +340 -0
- package/dist/verification/index.d.ts +6 -0
- package/dist/verification/index.js +5 -0
- package/dist/verification/types.d.ts +133 -0
- package/dist/verification/types.js +5 -0
- package/dist/verification/verifier.d.ts +30 -0
- package/dist/verification/verifier.js +309 -0
- package/dist/version.d.ts +19 -0
- package/dist/version.js +48 -0
- package/dist/workflow/auto-generator.d.ts +27 -0
- package/dist/workflow/auto-generator.js +513 -0
- package/dist/workflow/discovery.d.ts +40 -0
- package/dist/workflow/discovery.js +195 -0
- package/dist/workflow/executor.d.ts +82 -0
- package/dist/workflow/executor.js +611 -0
- package/dist/workflow/index.d.ts +10 -0
- package/dist/workflow/index.js +10 -0
- package/dist/workflow/loader.d.ts +24 -0
- package/dist/workflow/loader.js +194 -0
- package/dist/workflow/state-tracker.d.ts +98 -0
- package/dist/workflow/state-tracker.js +424 -0
- package/dist/workflow/types.d.ts +337 -0
- package/dist/workflow/types.js +5 -0
- package/package.json +94 -0
- package/schemas/bellwether-check.schema.json +651 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { DEFAULT_MODELS, parseJSONResponse } from './client.js';
|
|
3
|
+
import { withRetry, LLM_RETRY_OPTIONS } from '../errors/retry.js';
|
|
4
|
+
import { LLMAuthError, LLMRateLimitError, LLMQuotaError, LLMConnectionError, LLMRefusalError, } from '../errors/index.js';
|
|
5
|
+
import { getLogger } from '../logging/logger.js';
|
|
6
|
+
import { LLM_DEFAULTS } from '../constants.js';
|
|
7
|
+
/**
|
|
8
|
+
* OpenAI LLM client implementation.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Models that require special parameter handling.
|
|
12
|
+
* This includes reasoning models (o1, o3) and newer GPT models (gpt-5+).
|
|
13
|
+
* These models:
|
|
14
|
+
* - Require max_completion_tokens instead of max_tokens
|
|
15
|
+
* - Don't support custom temperature (only default of 1)
|
|
16
|
+
* - Use reasoning tokens that come out of the completion token budget
|
|
17
|
+
*/
|
|
18
|
+
const MODELS_WITH_RESTRICTED_PARAMS = [
|
|
19
|
+
'o1',
|
|
20
|
+
'o1-mini',
|
|
21
|
+
'o1-preview',
|
|
22
|
+
'o3',
|
|
23
|
+
'o3-mini',
|
|
24
|
+
'gpt-5',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Minimum max_completion_tokens for reasoning models.
|
|
28
|
+
* Reasoning models use tokens for internal "thinking" before producing output.
|
|
29
|
+
* We need a high minimum to ensure there are enough tokens left for actual output.
|
|
30
|
+
*/
|
|
31
|
+
const REASONING_MODEL_MIN_TOKENS = 8192;
|
|
32
|
+
/**
|
|
33
|
+
* Check if a model has restricted parameters (newer reasoning/GPT-5 models).
|
|
34
|
+
*/
|
|
35
|
+
function hasRestrictedParams(model) {
|
|
36
|
+
const modelLower = model.toLowerCase();
|
|
37
|
+
return MODELS_WITH_RESTRICTED_PARAMS.some(prefix => modelLower.startsWith(prefix));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the effective max tokens for a model, accounting for reasoning overhead.
|
|
41
|
+
*/
|
|
42
|
+
function getEffectiveMaxTokens(model, requestedMaxTokens) {
|
|
43
|
+
if (hasRestrictedParams(model)) {
|
|
44
|
+
// Reasoning models need much higher token limits because reasoning tokens
|
|
45
|
+
// come out of the completion budget. Use at least REASONING_MODEL_MIN_TOKENS.
|
|
46
|
+
return Math.max(requestedMaxTokens, REASONING_MODEL_MIN_TOKENS);
|
|
47
|
+
}
|
|
48
|
+
return requestedMaxTokens;
|
|
49
|
+
}
|
|
50
|
+
export class OpenAIClient {
|
|
51
|
+
client;
|
|
52
|
+
defaultModel;
|
|
53
|
+
logger = getLogger('openai');
|
|
54
|
+
onUsage;
|
|
55
|
+
constructor(options) {
|
|
56
|
+
const apiKey = options?.apiKey ?? process.env.OPENAI_API_KEY;
|
|
57
|
+
if (!apiKey) {
|
|
58
|
+
throw new LLMAuthError('openai');
|
|
59
|
+
}
|
|
60
|
+
this.client = new OpenAI({
|
|
61
|
+
apiKey,
|
|
62
|
+
baseURL: options?.baseURL,
|
|
63
|
+
});
|
|
64
|
+
this.defaultModel = options?.model ?? DEFAULT_MODELS.openai;
|
|
65
|
+
this.onUsage = options?.onUsage;
|
|
66
|
+
}
|
|
67
|
+
getProviderInfo() {
|
|
68
|
+
return {
|
|
69
|
+
id: 'openai',
|
|
70
|
+
name: 'OpenAI',
|
|
71
|
+
supportsJSON: true,
|
|
72
|
+
supportsStreaming: true,
|
|
73
|
+
defaultModel: this.defaultModel,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async chat(messages, options) {
|
|
77
|
+
const model = options?.model ?? this.defaultModel;
|
|
78
|
+
return withRetry(async () => {
|
|
79
|
+
// Prepend system message if provided
|
|
80
|
+
const allMessages = options?.systemPrompt
|
|
81
|
+
? [{ role: 'system', content: options.systemPrompt }, ...messages]
|
|
82
|
+
: messages;
|
|
83
|
+
try {
|
|
84
|
+
// Build request parameters - newer models have restricted params
|
|
85
|
+
const requestedMaxTokens = options?.maxTokens ?? LLM_DEFAULTS.MAX_TOKENS;
|
|
86
|
+
const restrictedParams = hasRestrictedParams(model);
|
|
87
|
+
// For reasoning models, ensure we have enough tokens for both reasoning AND output
|
|
88
|
+
const maxTokensValue = getEffectiveMaxTokens(model, requestedMaxTokens);
|
|
89
|
+
const response = await this.client.chat.completions.create({
|
|
90
|
+
model,
|
|
91
|
+
messages: allMessages.map(m => ({
|
|
92
|
+
role: m.role,
|
|
93
|
+
content: m.content,
|
|
94
|
+
})),
|
|
95
|
+
// Use max_completion_tokens for newer models (o1, o3, gpt-5+)
|
|
96
|
+
// Use max_tokens for older models (gpt-4, gpt-3.5, etc.)
|
|
97
|
+
...(restrictedParams
|
|
98
|
+
? { max_completion_tokens: maxTokensValue }
|
|
99
|
+
: { max_tokens: maxTokensValue }),
|
|
100
|
+
// Newer models (o1, o3, gpt-5+) don't support custom temperature
|
|
101
|
+
// Only include temperature for models that support it
|
|
102
|
+
...(restrictedParams
|
|
103
|
+
? {}
|
|
104
|
+
: { temperature: options?.temperature ?? LLM_DEFAULTS.TEMPERATURE }),
|
|
105
|
+
response_format: options?.responseFormat === 'json'
|
|
106
|
+
? { type: 'json_object' }
|
|
107
|
+
: undefined,
|
|
108
|
+
});
|
|
109
|
+
// Track actual token usage from API response
|
|
110
|
+
if (this.onUsage && response.usage) {
|
|
111
|
+
this.onUsage(response.usage.prompt_tokens, response.usage.completion_tokens);
|
|
112
|
+
}
|
|
113
|
+
const choice = response.choices[0];
|
|
114
|
+
let content = choice?.message?.content;
|
|
115
|
+
// Check for refusal - but sometimes the model puts valid content in the refusal field!
|
|
116
|
+
if (!content && choice?.message?.refusal) {
|
|
117
|
+
const refusal = choice.message.refusal;
|
|
118
|
+
// Check if the refusal actually contains JSON (model mistake)
|
|
119
|
+
if (refusal.includes('[') || refusal.includes('{')) {
|
|
120
|
+
// Extract JSON from refusal text
|
|
121
|
+
const jsonMatch = refusal.match(/```json\s*([\s\S]*?)\s*```/) ||
|
|
122
|
+
refusal.match(/(\[[\s\S]*\])/) ||
|
|
123
|
+
refusal.match(/(\{[\s\S]*\})/);
|
|
124
|
+
if (jsonMatch) {
|
|
125
|
+
content = jsonMatch[1];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!content) {
|
|
129
|
+
throw new LLMRefusalError('openai', refusal, model);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!content) {
|
|
133
|
+
this.logger.error({ response: JSON.stringify(response) }, 'No content in OpenAI response');
|
|
134
|
+
throw new Error('No content in LLM response');
|
|
135
|
+
}
|
|
136
|
+
return content;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
// Convert to typed errors for retry logic
|
|
140
|
+
if (error instanceof Error) {
|
|
141
|
+
const message = error.message.toLowerCase();
|
|
142
|
+
if (message.includes('401')) {
|
|
143
|
+
throw new LLMAuthError('openai', model);
|
|
144
|
+
}
|
|
145
|
+
if (message.includes('429')) {
|
|
146
|
+
// Extract Retry-After header if available from OpenAI API error
|
|
147
|
+
let retryAfterMs;
|
|
148
|
+
const apiError = error;
|
|
149
|
+
if (apiError.headers?.get) {
|
|
150
|
+
const retryAfter = apiError.headers.get('retry-after');
|
|
151
|
+
if (retryAfter) {
|
|
152
|
+
// retry-after can be seconds or HTTP date
|
|
153
|
+
const seconds = parseInt(retryAfter, 10);
|
|
154
|
+
if (!isNaN(seconds)) {
|
|
155
|
+
retryAfterMs = seconds * 1000;
|
|
156
|
+
this.logger.debug({ retryAfterMs }, 'Extracted Retry-After header');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Also check x-ratelimit-reset-requests header (OpenAI specific)
|
|
160
|
+
const resetRequests = apiError.headers.get('x-ratelimit-reset-requests');
|
|
161
|
+
if (resetRequests && !retryAfterMs) {
|
|
162
|
+
// Format: "1s", "1m", etc.
|
|
163
|
+
const match = resetRequests.match(/(\d+)([smh])/);
|
|
164
|
+
if (match) {
|
|
165
|
+
const value = parseInt(match[1], 10);
|
|
166
|
+
const unit = match[2];
|
|
167
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000 };
|
|
168
|
+
retryAfterMs = value * (multipliers[unit] || 1000);
|
|
169
|
+
this.logger.debug({ retryAfterMs, resetRequests }, 'Extracted x-ratelimit-reset-requests');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
throw new LLMRateLimitError('openai', retryAfterMs, model);
|
|
174
|
+
}
|
|
175
|
+
if (message.includes('insufficient_quota')) {
|
|
176
|
+
throw new LLMQuotaError('openai', model);
|
|
177
|
+
}
|
|
178
|
+
if (message.includes('econnrefused') || message.includes('fetch failed')) {
|
|
179
|
+
throw new LLMConnectionError('openai', model);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}, {
|
|
185
|
+
...LLM_RETRY_OPTIONS,
|
|
186
|
+
operation: 'OpenAI chat completion',
|
|
187
|
+
context: { component: 'openai', metadata: { model } },
|
|
188
|
+
onRetry: (error, attempt, delayMs) => {
|
|
189
|
+
this.logger.debug({
|
|
190
|
+
attempt,
|
|
191
|
+
delayMs: Math.round(delayMs),
|
|
192
|
+
error: error instanceof Error ? error.message : String(error),
|
|
193
|
+
msg: `Retrying OpenAI API call`,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async complete(prompt, options) {
|
|
199
|
+
return this.chat([{ role: 'user', content: prompt }], options);
|
|
200
|
+
}
|
|
201
|
+
parseJSON(response) {
|
|
202
|
+
return parseJSONResponse(response);
|
|
203
|
+
}
|
|
204
|
+
async stream(prompt, options) {
|
|
205
|
+
return this.streamChat([{ role: 'user', content: prompt }], options);
|
|
206
|
+
}
|
|
207
|
+
async streamChat(messages, options) {
|
|
208
|
+
const model = options?.model ?? this.defaultModel;
|
|
209
|
+
return withRetry(async () => {
|
|
210
|
+
// Prepend system message if provided
|
|
211
|
+
const allMessages = options?.systemPrompt
|
|
212
|
+
? [{ role: 'system', content: options.systemPrompt }, ...messages]
|
|
213
|
+
: messages;
|
|
214
|
+
try {
|
|
215
|
+
// Build request parameters - newer models have restricted params
|
|
216
|
+
const requestedMaxTokens = options?.maxTokens ?? LLM_DEFAULTS.MAX_TOKENS;
|
|
217
|
+
const restrictedParams = hasRestrictedParams(model);
|
|
218
|
+
// For reasoning models, ensure we have enough tokens for both reasoning AND output
|
|
219
|
+
const maxTokensValue = getEffectiveMaxTokens(model, requestedMaxTokens);
|
|
220
|
+
const stream = await this.client.chat.completions.create({
|
|
221
|
+
model,
|
|
222
|
+
messages: allMessages.map(m => ({
|
|
223
|
+
role: m.role,
|
|
224
|
+
content: m.content,
|
|
225
|
+
})),
|
|
226
|
+
// Use max_completion_tokens for newer models (o1, o3, gpt-5+)
|
|
227
|
+
// Use max_tokens for older models (gpt-4, gpt-3.5, etc.)
|
|
228
|
+
...(restrictedParams
|
|
229
|
+
? { max_completion_tokens: maxTokensValue }
|
|
230
|
+
: { max_tokens: maxTokensValue }),
|
|
231
|
+
// Newer models (o1, o3, gpt-5+) don't support custom temperature
|
|
232
|
+
// Only include temperature for models that support it
|
|
233
|
+
...(restrictedParams
|
|
234
|
+
? {}
|
|
235
|
+
: { temperature: options?.temperature ?? LLM_DEFAULTS.TEMPERATURE }),
|
|
236
|
+
response_format: options?.responseFormat === 'json'
|
|
237
|
+
? { type: 'json_object' }
|
|
238
|
+
: undefined,
|
|
239
|
+
stream: true,
|
|
240
|
+
});
|
|
241
|
+
let fullText = '';
|
|
242
|
+
let inputTokens = 0;
|
|
243
|
+
let outputTokens = 0;
|
|
244
|
+
let finishReason = null;
|
|
245
|
+
for await (const chunk of stream) {
|
|
246
|
+
const choice = chunk.choices[0];
|
|
247
|
+
const content = choice?.delta?.content;
|
|
248
|
+
if (content) {
|
|
249
|
+
fullText += content;
|
|
250
|
+
options?.onChunk?.(content);
|
|
251
|
+
}
|
|
252
|
+
// Track finish reason from final chunk
|
|
253
|
+
if (choice?.finish_reason) {
|
|
254
|
+
finishReason = choice.finish_reason;
|
|
255
|
+
}
|
|
256
|
+
// Track usage from final chunk if available
|
|
257
|
+
if (chunk.usage) {
|
|
258
|
+
inputTokens = chunk.usage.prompt_tokens;
|
|
259
|
+
outputTokens = chunk.usage.completion_tokens;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Report token usage if callback provided
|
|
263
|
+
if (this.onUsage && (inputTokens > 0 || outputTokens > 0)) {
|
|
264
|
+
this.onUsage(inputTokens, outputTokens);
|
|
265
|
+
}
|
|
266
|
+
// Handle empty responses gracefully - don't throw, let caller handle
|
|
267
|
+
// This can happen with content filters, refusals, or empty model responses
|
|
268
|
+
if (!fullText && finishReason) {
|
|
269
|
+
this.logger.debug({ finishReason }, 'Streaming completed with no content');
|
|
270
|
+
}
|
|
271
|
+
options?.onComplete?.(fullText);
|
|
272
|
+
return { text: fullText, completed: fullText.length > 0 };
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
options?.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
276
|
+
// Convert to typed errors for retry logic (same as chat method)
|
|
277
|
+
if (error instanceof Error) {
|
|
278
|
+
const message = error.message.toLowerCase();
|
|
279
|
+
if (message.includes('401')) {
|
|
280
|
+
throw new LLMAuthError('openai', model);
|
|
281
|
+
}
|
|
282
|
+
if (message.includes('429')) {
|
|
283
|
+
let retryAfterMs;
|
|
284
|
+
const apiError = error;
|
|
285
|
+
if (apiError.headers?.get) {
|
|
286
|
+
const retryAfter = apiError.headers.get('retry-after');
|
|
287
|
+
if (retryAfter) {
|
|
288
|
+
const seconds = parseInt(retryAfter, 10);
|
|
289
|
+
if (!isNaN(seconds)) {
|
|
290
|
+
retryAfterMs = seconds * 1000;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
throw new LLMRateLimitError('openai', retryAfterMs, model);
|
|
295
|
+
}
|
|
296
|
+
if (message.includes('insufficient_quota')) {
|
|
297
|
+
throw new LLMQuotaError('openai', model);
|
|
298
|
+
}
|
|
299
|
+
if (message.includes('econnrefused') || message.includes('fetch failed')) {
|
|
300
|
+
throw new LLMConnectionError('openai', model);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}, {
|
|
306
|
+
...LLM_RETRY_OPTIONS,
|
|
307
|
+
operation: 'OpenAI streaming chat completion',
|
|
308
|
+
context: { component: 'openai', metadata: { model } },
|
|
309
|
+
onRetry: (error, attempt, delayMs) => {
|
|
310
|
+
this.logger.debug({
|
|
311
|
+
attempt,
|
|
312
|
+
delayMs: Math.round(delayMs),
|
|
313
|
+
error: error instanceof Error ? error.message : String(error),
|
|
314
|
+
msg: `Retrying OpenAI streaming API call`,
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token budget enforcement for LLM API calls.
|
|
3
|
+
*
|
|
4
|
+
* Provides pre-estimation, budget tracking, context window management,
|
|
5
|
+
* and graceful truncation.
|
|
6
|
+
*/
|
|
7
|
+
import type { Message, LLMClient, CompletionOptions, StreamingOptions, StreamingResult, ProviderInfo } from './client.js';
|
|
8
|
+
/**
|
|
9
|
+
* Token budget configuration options.
|
|
10
|
+
*/
|
|
11
|
+
export interface TokenBudgetOptions {
|
|
12
|
+
/** Maximum total tokens for the session (input + output) */
|
|
13
|
+
maxTotalTokens?: number;
|
|
14
|
+
/** Maximum input tokens per request */
|
|
15
|
+
maxInputTokensPerRequest?: number;
|
|
16
|
+
/** Maximum output tokens per request */
|
|
17
|
+
maxOutputTokensPerRequest?: number;
|
|
18
|
+
/** Warning threshold (0-1) - warn when this percentage of budget is used */
|
|
19
|
+
warningThreshold?: number;
|
|
20
|
+
/** Reserve tokens for output (don't use entire context window for input) */
|
|
21
|
+
outputReserve?: number;
|
|
22
|
+
/** Callback when approaching budget limit */
|
|
23
|
+
onBudgetWarning?: (used: number, total: number, percentage: number) => void;
|
|
24
|
+
/** Callback when budget is exceeded */
|
|
25
|
+
onBudgetExceeded?: (used: number, total: number) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Token estimation result.
|
|
29
|
+
*/
|
|
30
|
+
export interface TokenEstimate {
|
|
31
|
+
/** Estimated token count */
|
|
32
|
+
tokens: number;
|
|
33
|
+
/** Whether this exceeds the context window */
|
|
34
|
+
exceedsContext: boolean;
|
|
35
|
+
/** Context window size for the model */
|
|
36
|
+
contextWindow: number;
|
|
37
|
+
/** Available tokens after this input */
|
|
38
|
+
availableForOutput: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Budget status.
|
|
42
|
+
*/
|
|
43
|
+
export interface BudgetStatus {
|
|
44
|
+
/** Total tokens used */
|
|
45
|
+
totalUsed: number;
|
|
46
|
+
/** Total budget */
|
|
47
|
+
totalBudget: number;
|
|
48
|
+
/** Percentage of budget used */
|
|
49
|
+
percentageUsed: number;
|
|
50
|
+
/** Whether warning threshold is exceeded */
|
|
51
|
+
warningTriggered: boolean;
|
|
52
|
+
/** Whether budget is exceeded */
|
|
53
|
+
budgetExceeded: boolean;
|
|
54
|
+
/** Remaining tokens */
|
|
55
|
+
remaining: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Estimate tokens from text using a character-based heuristic.
|
|
59
|
+
* This is a rough approximation (~4 characters per token for English text).
|
|
60
|
+
*/
|
|
61
|
+
export declare function estimateTokens(text: string): number;
|
|
62
|
+
/**
|
|
63
|
+
* Estimate tokens for a message array.
|
|
64
|
+
*/
|
|
65
|
+
export declare function estimateMessagesTokens(messages: Message[]): number;
|
|
66
|
+
/**
|
|
67
|
+
* Get context window size for a model.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getContextWindow(model: string): number;
|
|
70
|
+
/**
|
|
71
|
+
* Estimate tokens and check against context window.
|
|
72
|
+
*/
|
|
73
|
+
export declare function estimateWithContext(text: string | Message[], model: string, outputReserve?: number): TokenEstimate;
|
|
74
|
+
/**
|
|
75
|
+
* Truncate messages to fit within token budget using sliding window.
|
|
76
|
+
* Keeps system message and most recent messages.
|
|
77
|
+
*/
|
|
78
|
+
export declare function truncateMessages(messages: Message[], maxTokens: number, options?: {
|
|
79
|
+
keepSystemMessage?: boolean;
|
|
80
|
+
minMessages?: number;
|
|
81
|
+
}): Message[];
|
|
82
|
+
/**
|
|
83
|
+
* Truncate text to approximately fit within token budget.
|
|
84
|
+
*/
|
|
85
|
+
export declare function truncateText(text: string, maxTokens: number): string;
|
|
86
|
+
/**
|
|
87
|
+
* Token budget tracker for a session.
|
|
88
|
+
*/
|
|
89
|
+
export declare class TokenBudgetTracker {
|
|
90
|
+
private readonly options;
|
|
91
|
+
private readonly onBudgetWarning?;
|
|
92
|
+
private readonly onBudgetExceeded?;
|
|
93
|
+
private totalInputTokens;
|
|
94
|
+
private totalOutputTokens;
|
|
95
|
+
private warningEmitted;
|
|
96
|
+
constructor(options?: TokenBudgetOptions);
|
|
97
|
+
/**
|
|
98
|
+
* Record token usage.
|
|
99
|
+
*/
|
|
100
|
+
recordUsage(inputTokens: number, outputTokens: number): void;
|
|
101
|
+
/**
|
|
102
|
+
* Get current budget status.
|
|
103
|
+
*/
|
|
104
|
+
getStatus(): BudgetStatus;
|
|
105
|
+
/**
|
|
106
|
+
* Check if a request would exceed the budget.
|
|
107
|
+
*/
|
|
108
|
+
wouldExceedBudget(estimatedInputTokens: number, expectedOutputTokens?: number): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Get maximum safe input tokens for next request.
|
|
111
|
+
*/
|
|
112
|
+
getMaxSafeInputTokens(): number;
|
|
113
|
+
/**
|
|
114
|
+
* Reset the tracker.
|
|
115
|
+
*/
|
|
116
|
+
reset(): void;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Budget-aware error.
|
|
120
|
+
*/
|
|
121
|
+
export declare class TokenBudgetExceededError extends Error {
|
|
122
|
+
readonly used: number;
|
|
123
|
+
readonly budget: number;
|
|
124
|
+
readonly requested: number;
|
|
125
|
+
constructor(used: number, budget: number, requested: number);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Wrapper that enforces token budget on an LLM client.
|
|
129
|
+
*/
|
|
130
|
+
export declare class BudgetEnforcedLLMClient implements LLMClient {
|
|
131
|
+
private readonly client;
|
|
132
|
+
private readonly tracker;
|
|
133
|
+
private readonly model;
|
|
134
|
+
private readonly strict;
|
|
135
|
+
constructor(client: LLMClient, options?: TokenBudgetOptions & {
|
|
136
|
+
model?: string;
|
|
137
|
+
strict?: boolean;
|
|
138
|
+
});
|
|
139
|
+
getProviderInfo(): ProviderInfo;
|
|
140
|
+
chat(messages: Message[], options?: CompletionOptions): Promise<string>;
|
|
141
|
+
complete(prompt: string, options?: CompletionOptions): Promise<string>;
|
|
142
|
+
stream(prompt: string, options?: StreamingOptions): Promise<StreamingResult>;
|
|
143
|
+
streamChat(messages: Message[], options?: StreamingOptions): Promise<StreamingResult>;
|
|
144
|
+
parseJSON<T>(response: string): T;
|
|
145
|
+
/**
|
|
146
|
+
* Get current budget status.
|
|
147
|
+
*/
|
|
148
|
+
getBudgetStatus(): BudgetStatus;
|
|
149
|
+
/**
|
|
150
|
+
* Reset the budget tracker.
|
|
151
|
+
*/
|
|
152
|
+
resetBudget(): void;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Create a budget-enforced wrapper around an LLM client.
|
|
156
|
+
*/
|
|
157
|
+
export declare function withTokenBudget(client: LLMClient, options?: TokenBudgetOptions & {
|
|
158
|
+
model?: string;
|
|
159
|
+
strict?: boolean;
|
|
160
|
+
}): BudgetEnforcedLLMClient;
|
|
161
|
+
//# sourceMappingURL=token-budget.d.ts.map
|