openlore 2.0.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/api/analyze.d.ts +17 -0
- package/dist/api/analyze.d.ts.map +1 -0
- package/dist/api/analyze.js +143 -0
- package/dist/api/analyze.js.map +1 -0
- package/dist/api/audit.d.ts +10 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +117 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/decisions.d.ts +55 -0
- package/dist/api/decisions.d.ts.map +1 -0
- package/dist/api/decisions.js +157 -0
- package/dist/api/decisions.js.map +1 -0
- package/dist/api/drift.d.ts +21 -0
- package/dist/api/drift.d.ts.map +1 -0
- package/dist/api/drift.js +152 -0
- package/dist/api/drift.js.map +1 -0
- package/dist/api/generate.d.ts +18 -0
- package/dist/api/generate.d.ts.map +1 -0
- package/dist/api/generate.js +259 -0
- package/dist/api/generate.js.map +1 -0
- package/dist/api/index.d.ts +41 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +34 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/init.d.ts +18 -0
- package/dist/api/init.d.ts.map +1 -0
- package/dist/api/init.js +83 -0
- package/dist/api/init.js.map +1 -0
- package/dist/api/run.d.ts +19 -0
- package/dist/api/run.d.ts.map +1 -0
- package/dist/api/run.js +312 -0
- package/dist/api/run.js.map +1 -0
- package/dist/api/specs.d.ts +49 -0
- package/dist/api/specs.d.ts.map +1 -0
- package/dist/api/specs.js +137 -0
- package/dist/api/specs.js.map +1 -0
- package/dist/api/types.d.ts +201 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +9 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/verify.d.ts +20 -0
- package/dist/api/verify.d.ts.map +1 -0
- package/dist/api/verify.js +117 -0
- package/dist/api/verify.js.map +1 -0
- package/dist/cli/commands/analyze.d.ts +30 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +683 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +9 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +98 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/decisions.d.ts +16 -0
- package/dist/cli/commands/decisions.d.ts.map +1 -0
- package/dist/cli/commands/decisions.js +864 -0
- package/dist/cli/commands/decisions.js.map +1 -0
- package/dist/cli/commands/digest.d.ts +9 -0
- package/dist/cli/commands/digest.d.ts.map +1 -0
- package/dist/cli/commands/digest.js +61 -0
- package/dist/cli/commands/digest.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +9 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +398 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/drift.d.ts +9 -0
- package/dist/cli/commands/drift.d.ts.map +1 -0
- package/dist/cli/commands/drift.js +550 -0
- package/dist/cli/commands/drift.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +9 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +565 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +173 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +2235 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +1384 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/refresh-stories.d.ts +10 -0
- package/dist/cli/commands/refresh-stories.d.ts.map +1 -0
- package/dist/cli/commands/refresh-stories.js +314 -0
- package/dist/cli/commands/refresh-stories.js.map +1 -0
- package/dist/cli/commands/run.d.ts +9 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +459 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +19 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +355 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/test.d.ts +22 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +180 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/commands/verify.d.ts +9 -0
- package/dist/cli/commands/verify.d.ts.map +1 -0
- package/dist/cli/commands/verify.js +383 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/commands/view.d.ts +13 -0
- package/dist/cli/commands/view.d.ts.map +1 -0
- package/dist/cli/commands/view.js +547 -0
- package/dist/cli/commands/view.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +118 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui-approval.d.ts +11 -0
- package/dist/cli/tui-approval.d.ts.map +1 -0
- package/dist/cli/tui-approval.js +129 -0
- package/dist/cli/tui-approval.js.map +1 -0
- package/dist/constants.d.ts +314 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +382 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/analyzer/ai-config-generator.d.ts +54 -0
- package/dist/core/analyzer/ai-config-generator.d.ts.map +1 -0
- package/dist/core/analyzer/ai-config-generator.js +98 -0
- package/dist/core/analyzer/ai-config-generator.js.map +1 -0
- package/dist/core/analyzer/architecture-writer.d.ts +67 -0
- package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
- package/dist/core/analyzer/architecture-writer.js +209 -0
- package/dist/core/analyzer/architecture-writer.js.map +1 -0
- package/dist/core/analyzer/artifact-generator.d.ts +261 -0
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
- package/dist/core/analyzer/artifact-generator.js +909 -0
- package/dist/core/analyzer/artifact-generator.js.map +1 -0
- package/dist/core/analyzer/ast-chunker.d.ts +24 -0
- package/dist/core/analyzer/ast-chunker.d.ts.map +1 -0
- package/dist/core/analyzer/ast-chunker.js +198 -0
- package/dist/core/analyzer/ast-chunker.js.map +1 -0
- package/dist/core/analyzer/call-graph.d.ts +162 -0
- package/dist/core/analyzer/call-graph.d.ts.map +1 -0
- package/dist/core/analyzer/call-graph.js +2040 -0
- package/dist/core/analyzer/call-graph.js.map +1 -0
- package/dist/core/analyzer/code-shaper.d.ts +33 -0
- package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
- package/dist/core/analyzer/code-shaper.js +154 -0
- package/dist/core/analyzer/code-shaper.js.map +1 -0
- package/dist/core/analyzer/codebase-digest.d.ts +40 -0
- package/dist/core/analyzer/codebase-digest.d.ts.map +1 -0
- package/dist/core/analyzer/codebase-digest.js +195 -0
- package/dist/core/analyzer/codebase-digest.js.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts +30 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.js +71 -0
- package/dist/core/analyzer/cpp-header-resolver.js.map +1 -0
- package/dist/core/analyzer/dependency-graph.d.ts +230 -0
- package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
- package/dist/core/analyzer/dependency-graph.js +752 -0
- package/dist/core/analyzer/dependency-graph.js.map +1 -0
- package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
- package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
- package/dist/core/analyzer/duplicate-detector.js +289 -0
- package/dist/core/analyzer/duplicate-detector.js.map +1 -0
- package/dist/core/analyzer/embedding-service.d.ts +56 -0
- package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
- package/dist/core/analyzer/embedding-service.js +118 -0
- package/dist/core/analyzer/embedding-service.js.map +1 -0
- package/dist/core/analyzer/env-extractor.d.ts +33 -0
- package/dist/core/analyzer/env-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/env-extractor.js +196 -0
- package/dist/core/analyzer/env-extractor.js.map +1 -0
- package/dist/core/analyzer/external-packages.d.ts +20 -0
- package/dist/core/analyzer/external-packages.d.ts.map +1 -0
- package/dist/core/analyzer/external-packages.js +175 -0
- package/dist/core/analyzer/external-packages.js.map +1 -0
- package/dist/core/analyzer/file-walker.d.ts +78 -0
- package/dist/core/analyzer/file-walker.d.ts.map +1 -0
- package/dist/core/analyzer/file-walker.js +532 -0
- package/dist/core/analyzer/file-walker.js.map +1 -0
- package/dist/core/analyzer/function-registry-trie.d.ts +21 -0
- package/dist/core/analyzer/function-registry-trie.d.ts.map +1 -0
- package/dist/core/analyzer/function-registry-trie.js +39 -0
- package/dist/core/analyzer/function-registry-trie.js.map +1 -0
- package/dist/core/analyzer/http-route-parser.d.ts +152 -0
- package/dist/core/analyzer/http-route-parser.d.ts.map +1 -0
- package/dist/core/analyzer/http-route-parser.js +971 -0
- package/dist/core/analyzer/http-route-parser.js.map +1 -0
- package/dist/core/analyzer/import-parser.d.ts +100 -0
- package/dist/core/analyzer/import-parser.d.ts.map +1 -0
- package/dist/core/analyzer/import-parser.js +952 -0
- package/dist/core/analyzer/import-parser.js.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts +25 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.js +99 -0
- package/dist/core/analyzer/import-resolver-bridge.js.map +1 -0
- package/dist/core/analyzer/index.d.ts +10 -0
- package/dist/core/analyzer/index.d.ts.map +1 -0
- package/dist/core/analyzer/index.js +10 -0
- package/dist/core/analyzer/index.js.map +1 -0
- package/dist/core/analyzer/middleware-extractor.d.ts +29 -0
- package/dist/core/analyzer/middleware-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/middleware-extractor.js +195 -0
- package/dist/core/analyzer/middleware-extractor.js.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts +83 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.js +351 -0
- package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
- package/dist/core/analyzer/repository-mapper.d.ts +150 -0
- package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
- package/dist/core/analyzer/repository-mapper.js +740 -0
- package/dist/core/analyzer/repository-mapper.js.map +1 -0
- package/dist/core/analyzer/schema-extractor.d.ts +41 -0
- package/dist/core/analyzer/schema-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/schema-extractor.js +229 -0
- package/dist/core/analyzer/schema-extractor.js.map +1 -0
- package/dist/core/analyzer/signature-extractor.d.ts +31 -0
- package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/signature-extractor.js +675 -0
- package/dist/core/analyzer/signature-extractor.js.map +1 -0
- package/dist/core/analyzer/significance-scorer.d.ts +79 -0
- package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
- package/dist/core/analyzer/significance-scorer.js +407 -0
- package/dist/core/analyzer/significance-scorer.js.map +1 -0
- package/dist/core/analyzer/spec-snapshot-generator.d.ts +17 -0
- package/dist/core/analyzer/spec-snapshot-generator.d.ts.map +1 -0
- package/dist/core/analyzer/spec-snapshot-generator.js +201 -0
- package/dist/core/analyzer/spec-snapshot-generator.js.map +1 -0
- package/dist/core/analyzer/spec-vector-index.d.ts +68 -0
- package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/spec-vector-index.js +340 -0
- package/dist/core/analyzer/spec-vector-index.js.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts +51 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.js +147 -0
- package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
- package/dist/core/analyzer/type-inference-engine.d.ts +23 -0
- package/dist/core/analyzer/type-inference-engine.d.ts.map +1 -0
- package/dist/core/analyzer/type-inference-engine.js +130 -0
- package/dist/core/analyzer/type-inference-engine.js.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts +43 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.js +245 -0
- package/dist/core/analyzer/ui-component-extractor.js.map +1 -0
- package/dist/core/analyzer/unified-search.d.ts +116 -0
- package/dist/core/analyzer/unified-search.d.ts.map +1 -0
- package/dist/core/analyzer/unified-search.js +231 -0
- package/dist/core/analyzer/unified-search.js.map +1 -0
- package/dist/core/analyzer/vector-index.d.ts +92 -0
- package/dist/core/analyzer/vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/vector-index.js +451 -0
- package/dist/core/analyzer/vector-index.js.map +1 -0
- package/dist/core/decisions/consolidator.d.ts +14 -0
- package/dist/core/decisions/consolidator.d.ts.map +1 -0
- package/dist/core/decisions/consolidator.js +169 -0
- package/dist/core/decisions/consolidator.js.map +1 -0
- package/dist/core/decisions/extractor.d.ts +26 -0
- package/dist/core/decisions/extractor.d.ts.map +1 -0
- package/dist/core/decisions/extractor.js +156 -0
- package/dist/core/decisions/extractor.js.map +1 -0
- package/dist/core/decisions/index.d.ts +19 -0
- package/dist/core/decisions/index.d.ts.map +1 -0
- package/dist/core/decisions/index.js +16 -0
- package/dist/core/decisions/index.js.map +1 -0
- package/dist/core/decisions/store.d.ts +36 -0
- package/dist/core/decisions/store.d.ts.map +1 -0
- package/dist/core/decisions/store.js +109 -0
- package/dist/core/decisions/store.js.map +1 -0
- package/dist/core/decisions/syncer.d.ts +27 -0
- package/dist/core/decisions/syncer.d.ts.map +1 -0
- package/dist/core/decisions/syncer.js +214 -0
- package/dist/core/decisions/syncer.js.map +1 -0
- package/dist/core/decisions/verifier.d.ts +20 -0
- package/dist/core/decisions/verifier.d.ts.map +1 -0
- package/dist/core/decisions/verifier.js +115 -0
- package/dist/core/decisions/verifier.js.map +1 -0
- package/dist/core/digest/digest-generator.d.ts +29 -0
- package/dist/core/digest/digest-generator.d.ts.map +1 -0
- package/dist/core/digest/digest-generator.js +181 -0
- package/dist/core/digest/digest-generator.js.map +1 -0
- package/dist/core/drift/drift-detector.d.ts +102 -0
- package/dist/core/drift/drift-detector.d.ts.map +1 -0
- package/dist/core/drift/drift-detector.js +598 -0
- package/dist/core/drift/drift-detector.js.map +1 -0
- package/dist/core/drift/git-diff.d.ts +60 -0
- package/dist/core/drift/git-diff.d.ts.map +1 -0
- package/dist/core/drift/git-diff.js +383 -0
- package/dist/core/drift/git-diff.js.map +1 -0
- package/dist/core/drift/index.d.ts +12 -0
- package/dist/core/drift/index.d.ts.map +1 -0
- package/dist/core/drift/index.js +9 -0
- package/dist/core/drift/index.js.map +1 -0
- package/dist/core/drift/spec-mapper.d.ts +73 -0
- package/dist/core/drift/spec-mapper.d.ts.map +1 -0
- package/dist/core/drift/spec-mapper.js +353 -0
- package/dist/core/drift/spec-mapper.js.map +1 -0
- package/dist/core/drift/test-suggester.d.ts +18 -0
- package/dist/core/drift/test-suggester.d.ts.map +1 -0
- package/dist/core/drift/test-suggester.js +107 -0
- package/dist/core/drift/test-suggester.js.map +1 -0
- package/dist/core/generator/adr-generator.d.ts +32 -0
- package/dist/core/generator/adr-generator.d.ts.map +1 -0
- package/dist/core/generator/adr-generator.js +192 -0
- package/dist/core/generator/adr-generator.js.map +1 -0
- package/dist/core/generator/index.d.ts +9 -0
- package/dist/core/generator/index.d.ts.map +1 -0
- package/dist/core/generator/index.js +12 -0
- package/dist/core/generator/index.js.map +1 -0
- package/dist/core/generator/mapping-generator.d.ts +54 -0
- package/dist/core/generator/mapping-generator.d.ts.map +1 -0
- package/dist/core/generator/mapping-generator.js +240 -0
- package/dist/core/generator/mapping-generator.js.map +1 -0
- package/dist/core/generator/openspec-compat.d.ts +160 -0
- package/dist/core/generator/openspec-compat.d.ts.map +1 -0
- package/dist/core/generator/openspec-compat.js +524 -0
- package/dist/core/generator/openspec-compat.js.map +1 -0
- package/dist/core/generator/openspec-format-generator.d.ts +131 -0
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
- package/dist/core/generator/openspec-format-generator.js +963 -0
- package/dist/core/generator/openspec-format-generator.js.map +1 -0
- package/dist/core/generator/openspec-writer.d.ts +130 -0
- package/dist/core/generator/openspec-writer.d.ts.map +1 -0
- package/dist/core/generator/openspec-writer.js +404 -0
- package/dist/core/generator/openspec-writer.js.map +1 -0
- package/dist/core/generator/prompts.d.ts +35 -0
- package/dist/core/generator/prompts.d.ts.map +1 -0
- package/dist/core/generator/prompts.js +212 -0
- package/dist/core/generator/prompts.js.map +1 -0
- package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
- package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
- package/dist/core/generator/rag-manifest-generator.js +134 -0
- package/dist/core/generator/rag-manifest-generator.js.map +1 -0
- package/dist/core/generator/schemas.d.ts +365 -0
- package/dist/core/generator/schemas.d.ts.map +1 -0
- package/dist/core/generator/schemas.js +190 -0
- package/dist/core/generator/schemas.js.map +1 -0
- package/dist/core/generator/spec-pipeline.d.ts +123 -0
- package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
- package/dist/core/generator/spec-pipeline.js +699 -0
- package/dist/core/generator/spec-pipeline.js.map +1 -0
- package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
- package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
- package/dist/core/generator/stages/stage1-survey.js +171 -0
- package/dist/core/generator/stages/stage1-survey.js.map +1 -0
- package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
- package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
- package/dist/core/generator/stages/stage2-entities.js +74 -0
- package/dist/core/generator/stages/stage2-entities.js.map +1 -0
- package/dist/core/generator/stages/stage3-services.d.ts +11 -0
- package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
- package/dist/core/generator/stages/stage3-services.js +85 -0
- package/dist/core/generator/stages/stage3-services.js.map +1 -0
- package/dist/core/generator/stages/stage4-api.d.ts +11 -0
- package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
- package/dist/core/generator/stages/stage4-api.js +72 -0
- package/dist/core/generator/stages/stage4-api.js.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts +11 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.js +75 -0
- package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
- package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
- package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
- package/dist/core/generator/stages/stage6-adr.js +47 -0
- package/dist/core/generator/stages/stage6-adr.js.map +1 -0
- package/dist/core/services/chat-agent.d.ts +50 -0
- package/dist/core/services/chat-agent.d.ts.map +1 -0
- package/dist/core/services/chat-agent.js +369 -0
- package/dist/core/services/chat-agent.js.map +1 -0
- package/dist/core/services/chat-tools.d.ts +32 -0
- package/dist/core/services/chat-tools.d.ts.map +1 -0
- package/dist/core/services/chat-tools.js +494 -0
- package/dist/core/services/chat-tools.js.map +1 -0
- package/dist/core/services/config-manager.d.ts +61 -0
- package/dist/core/services/config-manager.d.ts.map +1 -0
- package/dist/core/services/config-manager.js +149 -0
- package/dist/core/services/config-manager.js.map +1 -0
- package/dist/core/services/edge-store.d.ts +57 -0
- package/dist/core/services/edge-store.d.ts.map +1 -0
- package/dist/core/services/edge-store.js +419 -0
- package/dist/core/services/edge-store.js.map +1 -0
- package/dist/core/services/gitignore-manager.d.ts +29 -0
- package/dist/core/services/gitignore-manager.d.ts.map +1 -0
- package/dist/core/services/gitignore-manager.js +95 -0
- package/dist/core/services/gitignore-manager.js.map +1 -0
- package/dist/core/services/index.d.ts +8 -0
- package/dist/core/services/index.d.ts.map +1 -0
- package/dist/core/services/index.js +8 -0
- package/dist/core/services/index.js.map +1 -0
- package/dist/core/services/llm-service.d.ts +379 -0
- package/dist/core/services/llm-service.d.ts.map +1 -0
- package/dist/core/services/llm-service.js +1553 -0
- package/dist/core/services/llm-service.js.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts +127 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.js +1185 -0
- package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
- package/dist/core/services/mcp-handlers/change.d.ts +14 -0
- package/dist/core/services/mcp-handlers/change.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/change.js +416 -0
- package/dist/core/services/mcp-handlers/change.js.map +1 -0
- package/dist/core/services/mcp-handlers/decisions.d.ts +16 -0
- package/dist/core/services/mcp-handlers/decisions.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/decisions.js +239 -0
- package/dist/core/services/mcp-handlers/decisions.js.map +1 -0
- package/dist/core/services/mcp-handlers/graph.d.ts +94 -0
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/graph.js +693 -0
- package/dist/core/services/mcp-handlers/graph.js.map +1 -0
- package/dist/core/services/mcp-handlers/orient.d.ts +17 -0
- package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/orient.js +357 -0
- package/dist/core/services/mcp-handlers/orient.js.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts +66 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.js +432 -0
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
- package/dist/core/services/mcp-handlers/utils.d.ts +85 -0
- package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/utils.js +262 -0
- package/dist/core/services/mcp-handlers/utils.js.map +1 -0
- package/dist/core/services/mcp-watcher.d.ts +41 -0
- package/dist/core/services/mcp-watcher.d.ts.map +1 -0
- package/dist/core/services/mcp-watcher.js +254 -0
- package/dist/core/services/mcp-watcher.js.map +1 -0
- package/dist/core/services/project-detector.d.ts +32 -0
- package/dist/core/services/project-detector.d.ts.map +1 -0
- package/dist/core/services/project-detector.js +100 -0
- package/dist/core/services/project-detector.js.map +1 -0
- package/dist/core/test-generator/coverage-analyzer.d.ts +27 -0
- package/dist/core/test-generator/coverage-analyzer.d.ts.map +1 -0
- package/dist/core/test-generator/coverage-analyzer.js +285 -0
- package/dist/core/test-generator/coverage-analyzer.js.map +1 -0
- package/dist/core/test-generator/framework-detector.d.ts +17 -0
- package/dist/core/test-generator/framework-detector.d.ts.map +1 -0
- package/dist/core/test-generator/framework-detector.js +65 -0
- package/dist/core/test-generator/framework-detector.js.map +1 -0
- package/dist/core/test-generator/index.d.ts +14 -0
- package/dist/core/test-generator/index.d.ts.map +1 -0
- package/dist/core/test-generator/index.js +11 -0
- package/dist/core/test-generator/index.js.map +1 -0
- package/dist/core/test-generator/renderers/catch2.d.ts +8 -0
- package/dist/core/test-generator/renderers/catch2.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/catch2.js +47 -0
- package/dist/core/test-generator/renderers/catch2.js.map +1 -0
- package/dist/core/test-generator/renderers/gtest.d.ts +8 -0
- package/dist/core/test-generator/renderers/gtest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/gtest.js +45 -0
- package/dist/core/test-generator/renderers/gtest.js.map +1 -0
- package/dist/core/test-generator/renderers/index.d.ts +20 -0
- package/dist/core/test-generator/renderers/index.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/index.js +35 -0
- package/dist/core/test-generator/renderers/index.js.map +1 -0
- package/dist/core/test-generator/renderers/playwright.d.ts +8 -0
- package/dist/core/test-generator/renderers/playwright.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/playwright.js +44 -0
- package/dist/core/test-generator/renderers/playwright.js.map +1 -0
- package/dist/core/test-generator/renderers/pytest.d.ts +8 -0
- package/dist/core/test-generator/renderers/pytest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/pytest.js +44 -0
- package/dist/core/test-generator/renderers/pytest.js.map +1 -0
- package/dist/core/test-generator/renderers/shared.d.ts +21 -0
- package/dist/core/test-generator/renderers/shared.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/shared.js +56 -0
- package/dist/core/test-generator/renderers/shared.js.map +1 -0
- package/dist/core/test-generator/renderers/vitest.d.ts +8 -0
- package/dist/core/test-generator/renderers/vitest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/vitest.js +52 -0
- package/dist/core/test-generator/renderers/vitest.js.map +1 -0
- package/dist/core/test-generator/scenario-parser.d.ts +33 -0
- package/dist/core/test-generator/scenario-parser.d.ts.map +1 -0
- package/dist/core/test-generator/scenario-parser.js +244 -0
- package/dist/core/test-generator/scenario-parser.js.map +1 -0
- package/dist/core/test-generator/test-generator.d.ts +30 -0
- package/dist/core/test-generator/test-generator.d.ts.map +1 -0
- package/dist/core/test-generator/test-generator.js +174 -0
- package/dist/core/test-generator/test-generator.js.map +1 -0
- package/dist/core/test-generator/test-writer.d.ts +25 -0
- package/dist/core/test-generator/test-writer.d.ts.map +1 -0
- package/dist/core/test-generator/test-writer.js +128 -0
- package/dist/core/test-generator/test-writer.js.map +1 -0
- package/dist/core/test-generator/then-matchers.d.ts +35 -0
- package/dist/core/test-generator/then-matchers.d.ts.map +1 -0
- package/dist/core/test-generator/then-matchers.js +211 -0
- package/dist/core/test-generator/then-matchers.js.map +1 -0
- package/dist/core/verifier/index.d.ts +5 -0
- package/dist/core/verifier/index.d.ts.map +1 -0
- package/dist/core/verifier/index.js +5 -0
- package/dist/core/verifier/index.js.map +1 -0
- package/dist/core/verifier/verification-engine.d.ts +293 -0
- package/dist/core/verifier/verification-engine.d.ts.map +1 -0
- package/dist/core/verifier/verification-engine.js +919 -0
- package/dist/core/verifier/verification-engine.js.map +1 -0
- package/dist/types/index.d.ts +368 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/pipeline.d.ts +167 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +5 -0
- package/dist/types/pipeline.js.map +1 -0
- package/dist/types/test-generator.d.ts +103 -0
- package/dist/types/test-generator.d.ts.map +1 -0
- package/dist/types/test-generator.js +17 -0
- package/dist/types/test-generator.js.map +1 -0
- package/dist/utils/command-helpers.d.ts +68 -0
- package/dist/utils/command-helpers.d.ts.map +1 -0
- package/dist/utils/command-helpers.js +150 -0
- package/dist/utils/command-helpers.js.map +1 -0
- package/dist/utils/errors.d.ts +51 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +129 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +149 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +342 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/misc.d.ts +10 -0
- package/dist/utils/misc.d.ts.map +1 -0
- package/dist/utils/misc.js +21 -0
- package/dist/utils/misc.js.map +1 -0
- package/dist/utils/progress.d.ts +142 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +283 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/prompts.d.ts +53 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +199 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/shutdown.d.ts +89 -0
- package/dist/utils/shutdown.d.ts.map +1 -0
- package/dist/utils/shutdown.js +238 -0
- package/dist/utils/shutdown.js.map +1 -0
- package/examples/bmad/README.md +113 -0
- package/examples/bmad/agents/architect.md +226 -0
- package/examples/bmad/agents/dev-brownfield.md +69 -0
- package/examples/bmad/setup/architect.customize.yaml +14 -0
- package/examples/bmad/tasks/implement-story.md +254 -0
- package/examples/bmad/tasks/onboarding.md +169 -0
- package/examples/bmad/tasks/refactor.md +178 -0
- package/examples/bmad/tasks/sprint-planning.md +168 -0
- package/examples/bmad/templates/story.md +108 -0
- package/examples/cline-workflows/openlore-analyze-codebase.md +101 -0
- package/examples/cline-workflows/openlore-check-spec-drift.md +102 -0
- package/examples/cline-workflows/openlore-execute-refactor.md +212 -0
- package/examples/cline-workflows/openlore-implement-feature.md +266 -0
- package/examples/cline-workflows/openlore-plan-refactor.md +279 -0
- package/examples/cline-workflows/openlore-refactor-codebase.md +16 -0
- package/examples/cline-workflows/openlore-write-tests.md +177 -0
- package/examples/drift-demo/openspec/config.yaml +14 -0
- package/examples/drift-demo/openspec/specs/architecture/spec.md +30 -0
- package/examples/drift-demo/openspec/specs/auth/spec.md +71 -0
- package/examples/drift-demo/openspec/specs/database/spec.md +33 -0
- package/examples/drift-demo/openspec/specs/overview/spec.md +20 -0
- package/examples/drift-demo/openspec/specs/projects/spec.md +55 -0
- package/examples/drift-demo/openspec/specs/tasks/spec.md +78 -0
- package/examples/drift-demo/package.json +21 -0
- package/examples/drift-demo/src/auth/auth-middleware.ts +30 -0
- package/examples/drift-demo/src/auth/auth-routes.ts +29 -0
- package/examples/drift-demo/src/auth/auth-service.ts +45 -0
- package/examples/drift-demo/src/database/connection.ts +27 -0
- package/examples/drift-demo/src/index.ts +16 -0
- package/examples/drift-demo/src/projects/project-model.ts +15 -0
- package/examples/drift-demo/src/projects/project-service.ts +34 -0
- package/examples/drift-demo/src/tasks/task-model.ts +37 -0
- package/examples/drift-demo/src/tasks/task-routes.ts +53 -0
- package/examples/drift-demo/src/tasks/task-service.ts +60 -0
- package/examples/drift-demo/src/utils/validation.ts +11 -0
- package/examples/drift-demo/tests/auth.test.ts +4 -0
- package/examples/drift-demo/tests/tasks.test.ts +4 -0
- package/examples/drift-demo/tsconfig.json +10 -0
- package/examples/drift-test/run-drift-test.sh +1087 -0
- package/examples/gsd/README.md +119 -0
- package/examples/gsd/commands/gsd/openlore-drift.md +111 -0
- package/examples/gsd/commands/gsd/openlore-orient.md +191 -0
- package/examples/mistral-vibe/README.md +101 -0
- package/examples/mistral-vibe/antipatterns-template.md +18 -0
- package/examples/mistral-vibe/skills/openlore-analyze-codebase/SKILL.md +124 -0
- package/examples/mistral-vibe/skills/openlore-brainstorm/SKILL.md +379 -0
- package/examples/mistral-vibe/skills/openlore-debug/SKILL.md +330 -0
- package/examples/mistral-vibe/skills/openlore-execute-refactor/SKILL.md +291 -0
- package/examples/mistral-vibe/skills/openlore-generate/SKILL.md +245 -0
- package/examples/mistral-vibe/skills/openlore-implement-story/SKILL.md +326 -0
- package/examples/mistral-vibe/skills/openlore-plan-refactor/SKILL.md +365 -0
- package/examples/mistral-vibe/skills/openlore-review-changes/SKILL.md +128 -0
- package/examples/mistral-vibe/skills/openlore-write-tests/SKILL.md +261 -0
- package/examples/opencode/agent-guard.ts +170 -0
- package/examples/opencode/plugins/anti-laziness.ts +202 -0
- package/examples/opencode/plugins/lib/openlore-context-injector-helpers.ts +116 -0
- package/examples/opencode/plugins/lib/openlore-decision-extractor-helpers.ts +65 -0
- package/examples/opencode/plugins/openlore-context-injector.test.ts +211 -0
- package/examples/opencode/plugins/openlore-context-injector.ts +165 -0
- package/examples/opencode/plugins/openlore-decision-extractor.test.ts +131 -0
- package/examples/opencode/plugins/openlore-decision-extractor.ts +322 -0
- package/examples/opencode/plugins/openlore-enforcer.ts +227 -0
- package/examples/opencode/prompts/sisyphus-sdd.md +150 -0
- package/examples/opencode-skills/openlore-analyze-codebase/SKILL.md +101 -0
- package/examples/opencode-skills/openlore-brainstorm/SKILL.md +354 -0
- package/examples/opencode-skills/openlore-debug/SKILL.md +291 -0
- package/examples/opencode-skills/openlore-execute-refactor/SKILL.md +241 -0
- package/examples/opencode-skills/openlore-generate/SKILL.md +236 -0
- package/examples/opencode-skills/openlore-implement-story/SKILL.md +251 -0
- package/examples/opencode-skills/openlore-plan-refactor/SKILL.md +298 -0
- package/examples/opencode-skills/openlore-review-changes/SKILL.md +134 -0
- package/examples/opencode-skills/openlore-write-tests/SKILL.md +230 -0
- package/examples/openspec-analysis/README.md +59 -0
- package/examples/openspec-analysis/SUMMARY.md +72 -0
- package/examples/openspec-analysis/config.json +16 -0
- package/examples/openspec-analysis/dependencies.mermaid +35 -0
- package/examples/openspec-analysis/dependency-graph.json +12116 -0
- package/examples/openspec-analysis/llm-context.json +119 -0
- package/examples/openspec-analysis/repo-structure.json +871 -0
- package/examples/openspec-cli/README.md +67 -0
- package/examples/openspec-cli/openspec/config.yaml +26 -0
- package/examples/openspec-cli/openspec/specs/architecture/spec.md +178 -0
- package/examples/openspec-cli/openspec/specs/artifact-graph/spec.md +143 -0
- package/examples/openspec-cli/openspec/specs/cli/spec.md +138 -0
- package/examples/openspec-cli/openspec/specs/overview/spec.md +60 -0
- package/examples/openspec-cli/openspec/specs/parsing/spec.md +123 -0
- package/examples/openspec-cli/openspec/specs/validation/spec.md +108 -0
- package/examples/spec-kit/README.md +104 -0
- package/examples/spec-kit/commands/drift.md +87 -0
- package/examples/spec-kit/commands/orient.md +138 -0
- package/examples/spec-kit/extension.yml +54 -0
- package/package.json +125 -0
- package/src/viewer/InteractiveGraphViewer.jsx +1600 -0
- package/src/viewer/app/index.html +17 -0
- package/src/viewer/app/main.jsx +13 -0
- package/src/viewer/components/ArchitectureView.jsx +177 -0
- package/src/viewer/components/ChatPanel.jsx +450 -0
- package/src/viewer/components/ClassGraph.jsx +782 -0
- package/src/viewer/components/ClusterGraph.jsx +469 -0
- package/src/viewer/components/FilterBar.jsx +179 -0
- package/src/viewer/components/FlatGraph.jsx +282 -0
- package/src/viewer/components/MicroComponents.jsx +85 -0
- package/src/viewer/hooks/usePanZoom.js +79 -0
- package/src/viewer/utils/constants.js +64 -0
- package/src/viewer/utils/graph-helpers.js +303 -0
- package/src/viewer/utils/graph-helpers.test.ts +39 -0
- package/src/viewer/utils/themes.js +206 -0
- package/stubs/tree-sitter-cli-stub/package.json +6 -0
|
@@ -0,0 +1,1553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Service
|
|
3
|
+
*
|
|
4
|
+
* Provides a clean interface for LLM interactions with proper error handling,
|
|
5
|
+
* retry logic, token management, and cost tracking.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import logger from '../../utils/logger.js';
|
|
10
|
+
import { CLAUDE_MAX_CONTEXT_TOKENS, CLAUDE_MAX_OUTPUT_TOKENS, MISTRAL_VIBE_MAX_CONTEXT_TOKENS, MISTRAL_VIBE_MAX_OUTPUT_TOKENS, LLM_CLI_MAX_BUFFER_BYTES, LLM_CLI_TIMEOUT_MS, DEFAULT_ANTHROPIC_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENAI_COMPAT_MODEL, DEFAULT_COPILOT_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_LLM_MAX_RETRIES, DEFAULT_LLM_INITIAL_DELAY_MS, DEFAULT_LLM_MAX_DELAY_MS, DEFAULT_LLM_TIMEOUT_MS, DEFAULT_LLM_COST_WARNING_THRESHOLD, CONTEXT_LIMIT_WARNING_RATIO, OPENLORE_DIR, OPENLORE_LOGS_SUBDIR, } from '../../constants.js';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// CLAUDE CODE PROVIDER (uses local `claude` CLI, no API key required)
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Claude Code CLI provider
|
|
16
|
+
*
|
|
17
|
+
* Routes LLM calls through the local `claude` CLI binary in non-interactive
|
|
18
|
+
* mode (`claude -p ...`). Authentication is handled by the Claude Code session
|
|
19
|
+
* (Max/Pro subscription) — no ANTHROPIC_API_KEY is required.
|
|
20
|
+
*/
|
|
21
|
+
export class ClaudeCodeProvider {
|
|
22
|
+
name = 'claude-code';
|
|
23
|
+
maxContextTokens = CLAUDE_MAX_CONTEXT_TOKENS;
|
|
24
|
+
maxOutputTokens = CLAUDE_MAX_OUTPUT_TOKENS;
|
|
25
|
+
model;
|
|
26
|
+
constructor(model) {
|
|
27
|
+
// Only pass --model if it looks like a Claude model name.
|
|
28
|
+
// Ignore the sentinel 'claude-code' string and non-Claude model names
|
|
29
|
+
// (e.g. 'mistral-large-latest' from a shared config).
|
|
30
|
+
this.model = model && model !== 'claude-code' && model.startsWith('claude-') ? model : undefined;
|
|
31
|
+
}
|
|
32
|
+
async generateCompletion(request) {
|
|
33
|
+
const { execFileSync } = await import('child_process');
|
|
34
|
+
// Claude Code CLI takes a single prompt; combine system + user prompts
|
|
35
|
+
const fullPrompt = request.systemPrompt
|
|
36
|
+
? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
|
|
37
|
+
: request.userPrompt;
|
|
38
|
+
const args = ['-p', fullPrompt, '--output-format', 'json'];
|
|
39
|
+
if (this.model)
|
|
40
|
+
args.push('--model', this.model);
|
|
41
|
+
// Remove Claude Code session env vars so the CLI can run inside an existing session
|
|
42
|
+
const env = { ...process.env };
|
|
43
|
+
delete env.CLAUDECODE;
|
|
44
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
45
|
+
delete env.CLAUDE_CODE_SSE_PORT;
|
|
46
|
+
delete env.CLAUDE_CODE_IDE_PORT;
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = execFileSync('claude', args, {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
maxBuffer: LLM_CLI_MAX_BUFFER_BYTES,
|
|
52
|
+
timeout: LLM_CLI_TIMEOUT_MS,
|
|
53
|
+
env,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const e = err;
|
|
58
|
+
const detail = e.stderr || e.stdout || e.message || String(err);
|
|
59
|
+
throw Object.assign(new Error(`claude CLI failed: ${detail}`), { retryable: false });
|
|
60
|
+
}
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(raw);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error(`claude CLI returned non-JSON output: ${raw.slice(0, 200)}`);
|
|
67
|
+
}
|
|
68
|
+
if (parsed.is_error) {
|
|
69
|
+
throw Object.assign(new Error(`claude CLI error: ${parsed.result}`), { retryable: false });
|
|
70
|
+
}
|
|
71
|
+
const inputTokens = parsed.usage?.input_tokens ?? estimateTokens(fullPrompt);
|
|
72
|
+
const outputTokens = parsed.usage?.output_tokens ?? estimateTokens(parsed.result ?? '');
|
|
73
|
+
return {
|
|
74
|
+
content: parsed.result ?? '',
|
|
75
|
+
usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
76
|
+
model: this.model ?? 'claude-code',
|
|
77
|
+
finishReason: 'stop',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
countTokens(text) {
|
|
81
|
+
return estimateTokens(text);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// MISTRAL VIBE PROVIDER (uses local `mistral-vibe` CLI, no API key required)
|
|
86
|
+
// ============================================================================
|
|
87
|
+
/**
|
|
88
|
+
* Mistral Vibe CLI provider
|
|
89
|
+
*
|
|
90
|
+
* Routes LLM calls through the local `mistral-vibe` CLI binary (standalone, no npm).
|
|
91
|
+
* No API key required — uses local LLM execution.
|
|
92
|
+
* If the binary is not on PATH, set MISTRAL_VIBE_CLI to its full path.
|
|
93
|
+
* The CLI is invoked as `vibe` (not `mistral-vibe`).
|
|
94
|
+
*/
|
|
95
|
+
export class MistralVibeProvider {
|
|
96
|
+
name = 'mistral-vibe';
|
|
97
|
+
maxContextTokens = MISTRAL_VIBE_MAX_CONTEXT_TOKENS;
|
|
98
|
+
maxOutputTokens = MISTRAL_VIBE_MAX_OUTPUT_TOKENS;
|
|
99
|
+
model;
|
|
100
|
+
constructor(model) {
|
|
101
|
+
// Ignore the sentinel 'mistral-vibe' string — let the CLI pick the default
|
|
102
|
+
this.model = model && model !== 'mistral-vibe' ? model : undefined;
|
|
103
|
+
}
|
|
104
|
+
async generateCompletion(request) {
|
|
105
|
+
const { execFileSync } = await import('child_process');
|
|
106
|
+
// Mistral Vibe CLI takes a single prompt; combine system + user prompts
|
|
107
|
+
const fullPrompt = request.systemPrompt
|
|
108
|
+
? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
|
|
109
|
+
: request.userPrompt;
|
|
110
|
+
// vibe CLI: -p for prompt, --output json for JSON, --agent for model/agent name
|
|
111
|
+
const args = ['-p', fullPrompt, '--output', 'json'];
|
|
112
|
+
if (this.model)
|
|
113
|
+
args.push('--agent', this.model);
|
|
114
|
+
// Use MISTRAL_VIBE_CLI if set (standalone install not on PATH), else 'vibe'
|
|
115
|
+
const mistralVibeBin = process.env.MISTRAL_VIBE_CLI ?? 'vibe';
|
|
116
|
+
let raw;
|
|
117
|
+
try {
|
|
118
|
+
raw = execFileSync(mistralVibeBin, args, {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
maxBuffer: LLM_CLI_MAX_BUFFER_BYTES,
|
|
121
|
+
timeout: LLM_CLI_TIMEOUT_MS,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const e = err;
|
|
126
|
+
const detail = e.stderr ?? e.stdout ?? e.message ?? String(err);
|
|
127
|
+
throw Object.assign(new Error(`mistral-vibe CLI failed: ${detail}`), { retryable: false });
|
|
128
|
+
}
|
|
129
|
+
// Defensive parsing: vibe --output json format is undocumented.
|
|
130
|
+
// Try multiple known shapes before falling back to raw text.
|
|
131
|
+
let content = '';
|
|
132
|
+
let inputTokens;
|
|
133
|
+
let outputTokens;
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
if (Array.isArray(parsed)) {
|
|
137
|
+
// Shape: [{role, content}, ...] — "all messages at end"
|
|
138
|
+
const msgs = parsed;
|
|
139
|
+
const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant');
|
|
140
|
+
content = String(lastAssistant?.content ?? '');
|
|
141
|
+
}
|
|
142
|
+
else if (typeof parsed === 'object' && parsed !== null) {
|
|
143
|
+
const p = parsed;
|
|
144
|
+
// Shape: {result: string, usage?: {...}} — Claude Code-style
|
|
145
|
+
if (typeof p.result === 'string') {
|
|
146
|
+
content = p.result;
|
|
147
|
+
const u = p.usage;
|
|
148
|
+
inputTokens = u?.input_tokens;
|
|
149
|
+
outputTokens = u?.output_tokens;
|
|
150
|
+
// Shape: {message: string} or {text: string} or {content: string}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
content = String(p.message ?? p.text ?? p.content ?? '');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// non-JSON output: use raw text
|
|
159
|
+
}
|
|
160
|
+
if (!content)
|
|
161
|
+
content = raw.trim();
|
|
162
|
+
inputTokens ??= estimateTokens(fullPrompt);
|
|
163
|
+
outputTokens ??= estimateTokens(content);
|
|
164
|
+
return {
|
|
165
|
+
content,
|
|
166
|
+
usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
167
|
+
model: this.model ?? 'mistral-vibe',
|
|
168
|
+
finishReason: 'stop',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
countTokens(text) {
|
|
172
|
+
return estimateTokens(text);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// SSL / FETCH HELPERS
|
|
177
|
+
// ============================================================================
|
|
178
|
+
/**
|
|
179
|
+
* Disable TLS certificate verification for all fetch requests in this process.
|
|
180
|
+
*
|
|
181
|
+
* Node.js native fetch does not support per-request TLS configuration.
|
|
182
|
+
* The only reliable cross-version approach is the NODE_TLS_REJECT_UNAUTHORIZED
|
|
183
|
+
* environment variable, which is process-global. This is set once and logged
|
|
184
|
+
* prominently so the user is aware.
|
|
185
|
+
*/
|
|
186
|
+
function disableSslVerification() {
|
|
187
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0')
|
|
188
|
+
return; // already disabled
|
|
189
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
190
|
+
// Warn prominently: this is process-global and affects all fetch calls.
|
|
191
|
+
console.warn('[openlore] WARNING: TLS certificate verification is DISABLED for this process.' +
|
|
192
|
+
' All HTTPS connections (including LLM API calls) are vulnerable to MITM attacks.' +
|
|
193
|
+
' Only use --insecure on trusted private networks with self-signed certificates.');
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Validate and normalise an API base URL.
|
|
197
|
+
* Returns the cleaned URL or throws on invalid input.
|
|
198
|
+
*/
|
|
199
|
+
function normalizeApiBase(url) {
|
|
200
|
+
// Must be a valid, absolute URL
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = new URL(url);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw new Error(`Invalid API base URL: "${url}". Must be a valid URL (e.g., http://localhost:8000/v1).`);
|
|
207
|
+
}
|
|
208
|
+
// Only allow http and https schemes
|
|
209
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
210
|
+
throw new Error(`Unsupported protocol in API base URL: "${parsed.protocol}". Only http and https are allowed.`);
|
|
211
|
+
}
|
|
212
|
+
// Strip trailing slashes for consistent path joining
|
|
213
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
214
|
+
}
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// RETRY-AFTER PARSING
|
|
217
|
+
// ============================================================================
|
|
218
|
+
/**
|
|
219
|
+
* Parse the number of milliseconds to wait before retrying a 429 response.
|
|
220
|
+
*
|
|
221
|
+
* Checks (in order):
|
|
222
|
+
* 1. Standard `Retry-After` HTTP header (seconds as integer, or HTTP-date)
|
|
223
|
+
* 2. `Limit resets at: YYYY-MM-DD HH:MM:SS UTC` in the response body
|
|
224
|
+
*
|
|
225
|
+
* Returns `undefined` when nothing useful is found so the caller can fall back
|
|
226
|
+
* to its own exponential-backoff delay.
|
|
227
|
+
*/
|
|
228
|
+
export function parseRetryAfterMs(body, retryAfterHeader) {
|
|
229
|
+
const BUFFER_MS = 500; // small buffer to avoid hitting the wall again immediately
|
|
230
|
+
// 1. Retry-After header
|
|
231
|
+
if (retryAfterHeader) {
|
|
232
|
+
const seconds = Number(retryAfterHeader);
|
|
233
|
+
if (!isNaN(seconds) && seconds > 0) {
|
|
234
|
+
return Math.ceil(seconds * 1000) + BUFFER_MS;
|
|
235
|
+
}
|
|
236
|
+
// HTTP-date format
|
|
237
|
+
const headerDate = Date.parse(retryAfterHeader);
|
|
238
|
+
if (!isNaN(headerDate)) {
|
|
239
|
+
const ms = headerDate - Date.now();
|
|
240
|
+
if (ms > 0)
|
|
241
|
+
return ms + BUFFER_MS;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// 2. "Limit resets at: YYYY-MM-DD HH:MM:SS UTC" in body
|
|
245
|
+
const match = body.match(/Limit resets at:\s*(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?\s*UTC)/i);
|
|
246
|
+
if (match) {
|
|
247
|
+
const resetMs = Date.parse(match[1].replace(' UTC', 'Z').replace(' ', 'T'));
|
|
248
|
+
if (!isNaN(resetMs)) {
|
|
249
|
+
const ms = resetMs - Date.now();
|
|
250
|
+
if (ms > 0)
|
|
251
|
+
return ms + BUFFER_MS;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
// ============================================================================
|
|
257
|
+
// PRICING (per 1M tokens)
|
|
258
|
+
// ============================================================================
|
|
259
|
+
const PRICING = {
|
|
260
|
+
anthropic: {
|
|
261
|
+
// Claude 4 family
|
|
262
|
+
'claude-opus-4': { input: 15.0, output: 75.0 },
|
|
263
|
+
'claude-sonnet-4': { input: 3.0, output: 15.0 },
|
|
264
|
+
'claude-haiku-4': { input: 0.80, output: 4.0 },
|
|
265
|
+
// Claude 3.7 / 3.5
|
|
266
|
+
'claude-3-7-sonnet': { input: 3.0, output: 15.0 },
|
|
267
|
+
'claude-3-5-sonnet': { input: 3.0, output: 15.0 },
|
|
268
|
+
'claude-3-5-haiku': { input: 0.80, output: 4.0 },
|
|
269
|
+
// Claude 3 (legacy)
|
|
270
|
+
'claude-3-opus': { input: 15.0, output: 75.0 },
|
|
271
|
+
'claude-3-sonnet': { input: 3.0, output: 15.0 },
|
|
272
|
+
'claude-3-haiku': { input: 0.25, output: 1.25 },
|
|
273
|
+
// Fallback: assume Sonnet-class pricing
|
|
274
|
+
default: { input: 3.0, output: 15.0 },
|
|
275
|
+
},
|
|
276
|
+
openai: {
|
|
277
|
+
// GPT-4o family
|
|
278
|
+
'gpt-4o': { input: 2.5, output: 10.0 },
|
|
279
|
+
'gpt-4o-mini': { input: 0.15, output: 0.6 },
|
|
280
|
+
// o-series reasoning models
|
|
281
|
+
'o1': { input: 15.0, output: 60.0 },
|
|
282
|
+
'o1-mini': { input: 3.0, output: 12.0 },
|
|
283
|
+
'o3': { input: 10.0, output: 40.0 },
|
|
284
|
+
'o3-mini': { input: 1.1, output: 4.4 },
|
|
285
|
+
'o4-mini': { input: 1.1, output: 4.4 },
|
|
286
|
+
// Legacy (still in use)
|
|
287
|
+
'gpt-4-turbo': { input: 10.0, output: 30.0 },
|
|
288
|
+
'gpt-4': { input: 30.0, output: 60.0 },
|
|
289
|
+
'gpt-3.5-turbo': { input: 0.5, output: 1.5 },
|
|
290
|
+
default: { input: 2.5, output: 10.0 },
|
|
291
|
+
},
|
|
292
|
+
'openai-compat': {
|
|
293
|
+
// Mistral
|
|
294
|
+
'mistral-large-latest': { input: 2.0, output: 6.0 },
|
|
295
|
+
'mistral-small-latest': { input: 0.1, output: 0.3 },
|
|
296
|
+
'codestral-latest': { input: 0.2, output: 0.6 },
|
|
297
|
+
// Groq
|
|
298
|
+
'llama-3.3-70b-versatile': { input: 0.59, output: 0.79 },
|
|
299
|
+
'llama-3.1-8b-instant': { input: 0.05, output: 0.08 },
|
|
300
|
+
default: { input: 1.0, output: 3.0 },
|
|
301
|
+
},
|
|
302
|
+
gemini: {
|
|
303
|
+
'gemini-2.0-flash': { input: 0.1, output: 0.4 },
|
|
304
|
+
'gemini-2.0-flash-lite': { input: 0.075, output: 0.3 },
|
|
305
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.0 },
|
|
306
|
+
'gemini-1.5-pro': { input: 1.25, output: 5.0 },
|
|
307
|
+
'gemini-1.5-flash': { input: 0.075, output: 0.3 },
|
|
308
|
+
default: { input: 0.1, output: 0.4 },
|
|
309
|
+
},
|
|
310
|
+
'claude-code': {
|
|
311
|
+
// No per-token cost: covered by Claude Max/Pro subscription
|
|
312
|
+
default: { input: 0, output: 0 },
|
|
313
|
+
},
|
|
314
|
+
'mistral-vibe': {
|
|
315
|
+
// No per-token cost: local CLI tool
|
|
316
|
+
default: { input: 0, output: 0 },
|
|
317
|
+
},
|
|
318
|
+
'gemini-cli': {
|
|
319
|
+
// No per-token cost: covered by Google account free tier
|
|
320
|
+
default: { input: 0, output: 0 },
|
|
321
|
+
},
|
|
322
|
+
'cursor-agent': {
|
|
323
|
+
// No per-token cost in openlore: Cursor subscription / CLI auth
|
|
324
|
+
default: { input: 0, output: 0 },
|
|
325
|
+
},
|
|
326
|
+
copilot: {
|
|
327
|
+
// No per-token cost: covered by GitHub Copilot subscription
|
|
328
|
+
default: { input: 0, output: 0 },
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Exported for use in pre-flight cost estimation.
|
|
333
|
+
* Look up pricing for a model ID using prefix/family matching.
|
|
334
|
+
* Exact match first, then longest prefix match, then provider default.
|
|
335
|
+
*
|
|
336
|
+
* This is robust to minor version suffixes like "claude-sonnet-4-6-20251120"
|
|
337
|
+
* matching the "claude-sonnet-4" family entry.
|
|
338
|
+
*/
|
|
339
|
+
export function lookupPricing(providerName, modelId) {
|
|
340
|
+
const table = PRICING[providerName] ?? PRICING.anthropic;
|
|
341
|
+
// 1. Exact match
|
|
342
|
+
if (table[modelId])
|
|
343
|
+
return table[modelId];
|
|
344
|
+
// 2. Longest prefix match (handles "claude-sonnet-4-6-20251120" → "claude-sonnet-4")
|
|
345
|
+
const modelLower = modelId.toLowerCase();
|
|
346
|
+
let bestKey = '';
|
|
347
|
+
for (const key of Object.keys(table)) {
|
|
348
|
+
if (key === 'default')
|
|
349
|
+
continue;
|
|
350
|
+
if (modelLower.startsWith(key) && key.length > bestKey.length) {
|
|
351
|
+
bestKey = key;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (bestKey)
|
|
355
|
+
return table[bestKey];
|
|
356
|
+
// 3. Provider default
|
|
357
|
+
return table.default ?? { input: 3.0, output: 15.0 };
|
|
358
|
+
}
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// TOKEN ESTIMATION
|
|
361
|
+
// ============================================================================
|
|
362
|
+
/**
|
|
363
|
+
* Estimate token count from text (rough approximation)
|
|
364
|
+
* ~4 characters per token for English text
|
|
365
|
+
*/
|
|
366
|
+
export function estimateTokens(text) {
|
|
367
|
+
// More accurate estimation considering code
|
|
368
|
+
// Code tends to have more tokens per character due to special chars
|
|
369
|
+
const codePatterns = /[{}()[\];:,.<>/\\|`~!@#$%^&*=+]/g;
|
|
370
|
+
const codeCharCount = (text.match(codePatterns) || []).length;
|
|
371
|
+
const regularCharCount = text.length - codeCharCount;
|
|
372
|
+
// Regular text: ~4 chars per token, code chars: ~2 chars per token
|
|
373
|
+
return Math.ceil(regularCharCount / 4 + codeCharCount / 2);
|
|
374
|
+
}
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// ANTHROPIC PROVIDER
|
|
377
|
+
// ============================================================================
|
|
378
|
+
/**
|
|
379
|
+
* Anthropic Claude provider
|
|
380
|
+
*/
|
|
381
|
+
export class AnthropicProvider {
|
|
382
|
+
name = 'anthropic';
|
|
383
|
+
maxContextTokens = 200000;
|
|
384
|
+
maxOutputTokens = 4096;
|
|
385
|
+
apiKey;
|
|
386
|
+
model;
|
|
387
|
+
baseUrl;
|
|
388
|
+
constructor(apiKey, model = DEFAULT_ANTHROPIC_MODEL, baseUrl, sslVerify = true) {
|
|
389
|
+
this.apiKey = apiKey;
|
|
390
|
+
this.model = model;
|
|
391
|
+
this.baseUrl = baseUrl ? normalizeApiBase(baseUrl) : 'https://api.anthropic.com/v1';
|
|
392
|
+
if (!sslVerify)
|
|
393
|
+
disableSslVerification();
|
|
394
|
+
}
|
|
395
|
+
countTokens(text) {
|
|
396
|
+
return estimateTokens(text);
|
|
397
|
+
}
|
|
398
|
+
async generateCompletion(request) {
|
|
399
|
+
const response = await fetch(`${this.baseUrl}/messages`, {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
headers: {
|
|
402
|
+
'Content-Type': 'application/json',
|
|
403
|
+
'x-api-key': this.apiKey,
|
|
404
|
+
'anthropic-version': '2023-06-01',
|
|
405
|
+
},
|
|
406
|
+
body: JSON.stringify({
|
|
407
|
+
model: this.model,
|
|
408
|
+
max_tokens: request.maxTokens ?? this.maxOutputTokens,
|
|
409
|
+
temperature: request.temperature ?? 0.3,
|
|
410
|
+
system: request.systemPrompt,
|
|
411
|
+
messages: [
|
|
412
|
+
{ role: 'user', content: request.userPrompt },
|
|
413
|
+
],
|
|
414
|
+
stop_sequences: request.stopSequences,
|
|
415
|
+
}),
|
|
416
|
+
});
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
const error = await response.text();
|
|
419
|
+
const errorObj = this.parseError(error, response.status, response.headers.get('retry-after'));
|
|
420
|
+
throw errorObj;
|
|
421
|
+
}
|
|
422
|
+
const data = await response.json();
|
|
423
|
+
const content = data.content
|
|
424
|
+
.filter(c => c.type === 'text')
|
|
425
|
+
.map(c => c.text)
|
|
426
|
+
.join('');
|
|
427
|
+
return {
|
|
428
|
+
content,
|
|
429
|
+
usage: {
|
|
430
|
+
inputTokens: data.usage.input_tokens,
|
|
431
|
+
outputTokens: data.usage.output_tokens,
|
|
432
|
+
totalTokens: data.usage.input_tokens + data.usage.output_tokens,
|
|
433
|
+
},
|
|
434
|
+
model: data.model,
|
|
435
|
+
finishReason: data.stop_reason === 'end_turn' ? 'stop' : data.stop_reason === 'max_tokens' ? 'length' : 'error',
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
parseError(error, status, retryAfterHeader) {
|
|
439
|
+
const detail = error.trim() || '(empty response body)';
|
|
440
|
+
const err = new Error(`HTTP ${status}: ${detail}`);
|
|
441
|
+
err.status = status;
|
|
442
|
+
err.retryable = status === 429 || status >= 500;
|
|
443
|
+
if (status === 429) {
|
|
444
|
+
err.retryAfterMs = parseRetryAfterMs(error, retryAfterHeader);
|
|
445
|
+
}
|
|
446
|
+
return err;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// OPENAI PROVIDER
|
|
451
|
+
// ============================================================================
|
|
452
|
+
/**
|
|
453
|
+
* Wrap a top-level array schema in an object so it satisfies OpenAI's
|
|
454
|
+
* structured-output requirement that the root type is "object".
|
|
455
|
+
* The existing unwrap logic in completeJSON (single-key object → array)
|
|
456
|
+
* reverses this transparently. See: #52
|
|
457
|
+
*/
|
|
458
|
+
function wrapArraySchema(schema) {
|
|
459
|
+
if (schema.type === 'array') {
|
|
460
|
+
return {
|
|
461
|
+
type: 'object',
|
|
462
|
+
properties: { items: schema },
|
|
463
|
+
required: ['items'],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return schema;
|
|
467
|
+
}
|
|
468
|
+
function isSchemaRecord(value) {
|
|
469
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
470
|
+
}
|
|
471
|
+
function schemaAllowsNull(schema) {
|
|
472
|
+
if (!isSchemaRecord(schema))
|
|
473
|
+
return false;
|
|
474
|
+
const type = schema.type;
|
|
475
|
+
if (type === 'null')
|
|
476
|
+
return true;
|
|
477
|
+
if (Array.isArray(type) && type.includes('null'))
|
|
478
|
+
return true;
|
|
479
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
480
|
+
const schemas = schema[key];
|
|
481
|
+
if (Array.isArray(schemas) && schemas.some(schemaAllowsNull)) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
function makeSchemaNullable(schema) {
|
|
488
|
+
if (schemaAllowsNull(schema))
|
|
489
|
+
return schema;
|
|
490
|
+
if (isSchemaRecord(schema)) {
|
|
491
|
+
const type = schema.type;
|
|
492
|
+
if (typeof type === 'string') {
|
|
493
|
+
schema.type = [type, 'null'];
|
|
494
|
+
return schema;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { anyOf: [schema, { type: 'null' }] };
|
|
498
|
+
}
|
|
499
|
+
function normalizeOpenAISchemaNode(node) {
|
|
500
|
+
if (Array.isArray(node)) {
|
|
501
|
+
for (const item of node) {
|
|
502
|
+
normalizeOpenAISchemaNode(item);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (!isSchemaRecord(node))
|
|
507
|
+
return;
|
|
508
|
+
const isObjectSchema = node.type === 'object' || (Array.isArray(node.type) && node.type.includes('object'));
|
|
509
|
+
if (isObjectSchema) {
|
|
510
|
+
const properties = isSchemaRecord(node.properties) ? node.properties : {};
|
|
511
|
+
const originalRequired = new Set(Array.isArray(node.required)
|
|
512
|
+
? node.required.filter((field) => typeof field === 'string')
|
|
513
|
+
: []);
|
|
514
|
+
node.properties = properties;
|
|
515
|
+
node.additionalProperties = false;
|
|
516
|
+
node.required = Object.keys(properties);
|
|
517
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
518
|
+
normalizeOpenAISchemaNode(propertySchema);
|
|
519
|
+
if (!originalRequired.has(key)) {
|
|
520
|
+
properties[key] = makeSchemaNullable(properties[key]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
for (const [key, value] of Object.entries(node)) {
|
|
525
|
+
if (isObjectSchema && key === 'properties')
|
|
526
|
+
continue;
|
|
527
|
+
normalizeOpenAISchemaNode(value);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function normalizeOpenAIResponseSchema(schema) {
|
|
531
|
+
const clonedSchema = JSON.parse(JSON.stringify(schema));
|
|
532
|
+
const wrappedSchema = wrapArraySchema(clonedSchema);
|
|
533
|
+
normalizeOpenAISchemaNode(wrappedSchema);
|
|
534
|
+
return wrappedSchema;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* OpenAI provider
|
|
538
|
+
*/
|
|
539
|
+
export class OpenAIProvider {
|
|
540
|
+
name = 'openai';
|
|
541
|
+
maxContextTokens = 128000;
|
|
542
|
+
maxOutputTokens = 4096;
|
|
543
|
+
apiKey;
|
|
544
|
+
model;
|
|
545
|
+
baseUrl;
|
|
546
|
+
constructor(apiKey, model = DEFAULT_OPENAI_MODEL, baseUrl, sslVerify = true) {
|
|
547
|
+
this.apiKey = apiKey;
|
|
548
|
+
this.model = model;
|
|
549
|
+
this.baseUrl = baseUrl ? normalizeApiBase(baseUrl) : 'https://api.openai.com/v1';
|
|
550
|
+
if (!sslVerify)
|
|
551
|
+
disableSslVerification();
|
|
552
|
+
}
|
|
553
|
+
countTokens(text) {
|
|
554
|
+
return estimateTokens(text);
|
|
555
|
+
}
|
|
556
|
+
async generateCompletion(request) {
|
|
557
|
+
const messages = [
|
|
558
|
+
{ role: 'system', content: request.systemPrompt },
|
|
559
|
+
{ role: 'user', content: request.userPrompt },
|
|
560
|
+
];
|
|
561
|
+
const body = {
|
|
562
|
+
model: this.model,
|
|
563
|
+
messages,
|
|
564
|
+
max_tokens: request.maxTokens ?? this.maxOutputTokens,
|
|
565
|
+
temperature: request.temperature ?? 0.3,
|
|
566
|
+
stop: request.stopSequences,
|
|
567
|
+
};
|
|
568
|
+
if (request.responseFormat === 'json' && request.jsonSchema) {
|
|
569
|
+
// Use OpenAI structured outputs when a JSON schema is provided.
|
|
570
|
+
// This forces the model to conform to the schema (e.g. start an array). (#26)
|
|
571
|
+
// Wrap top-level array schemas in an object to satisfy OpenAI's requirement. (#52)
|
|
572
|
+
body.response_format = {
|
|
573
|
+
type: 'json_schema',
|
|
574
|
+
json_schema: {
|
|
575
|
+
name: 'response',
|
|
576
|
+
schema: normalizeOpenAIResponseSchema(request.jsonSchema),
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
else if (request.responseFormat === 'json') {
|
|
581
|
+
body.response_format = { type: 'json_object' };
|
|
582
|
+
}
|
|
583
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
584
|
+
method: 'POST',
|
|
585
|
+
headers: {
|
|
586
|
+
'Content-Type': 'application/json',
|
|
587
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
588
|
+
},
|
|
589
|
+
body: JSON.stringify(body),
|
|
590
|
+
});
|
|
591
|
+
if (!response.ok) {
|
|
592
|
+
const error = await response.text();
|
|
593
|
+
const errorObj = this.parseError(error, response.status, response.headers.get('retry-after'));
|
|
594
|
+
throw errorObj;
|
|
595
|
+
}
|
|
596
|
+
const data = await response.json();
|
|
597
|
+
return {
|
|
598
|
+
content: data.choices[0]?.message?.content ?? '',
|
|
599
|
+
usage: {
|
|
600
|
+
inputTokens: data.usage.prompt_tokens,
|
|
601
|
+
outputTokens: data.usage.completion_tokens,
|
|
602
|
+
totalTokens: data.usage.total_tokens,
|
|
603
|
+
},
|
|
604
|
+
model: data.model,
|
|
605
|
+
finishReason: data.choices[0]?.finish_reason === 'stop' ? 'stop' : data.choices[0]?.finish_reason === 'length' ? 'length' : 'error',
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
parseError(error, status, retryAfterHeader) {
|
|
609
|
+
const detail = error.trim() || '(empty response body)';
|
|
610
|
+
const err = new Error(`HTTP ${status}: ${detail}`);
|
|
611
|
+
err.status = status;
|
|
612
|
+
err.retryable = status === 429 || status >= 500;
|
|
613
|
+
if (status === 429) {
|
|
614
|
+
err.retryAfterMs = parseRetryAfterMs(error, retryAfterHeader);
|
|
615
|
+
}
|
|
616
|
+
return err;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
export class OpenAICompatibleProvider {
|
|
620
|
+
name = 'openai-compat';
|
|
621
|
+
maxContextTokens = 128000;
|
|
622
|
+
maxOutputTokens = 4096;
|
|
623
|
+
apiKey;
|
|
624
|
+
model;
|
|
625
|
+
baseUrl;
|
|
626
|
+
disableResponseFormat;
|
|
627
|
+
constructor(apiKey, baseUrl, model = DEFAULT_OPENAI_COMPAT_MODEL, disableResponseFormat = false) {
|
|
628
|
+
this.apiKey = apiKey;
|
|
629
|
+
this.baseUrl = normalizeApiBase(baseUrl);
|
|
630
|
+
this.model = model;
|
|
631
|
+
this.disableResponseFormat = disableResponseFormat;
|
|
632
|
+
}
|
|
633
|
+
countTokens(text) {
|
|
634
|
+
return estimateTokens(text);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Fetch available models from the API endpoint
|
|
638
|
+
*/
|
|
639
|
+
async fetchAvailableModels() {
|
|
640
|
+
try {
|
|
641
|
+
const response = await fetch(`${this.baseUrl}/models`, {
|
|
642
|
+
method: 'GET',
|
|
643
|
+
headers: {
|
|
644
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
645
|
+
'Content-Type': 'application/json',
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
if (!response.ok) {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
const data = await response.json();
|
|
652
|
+
return data.data?.map(model => model.id).sort() ?? [];
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get known models for common API endpoints when /models is not available
|
|
660
|
+
*/
|
|
661
|
+
getKnownModelsForEndpoint() {
|
|
662
|
+
const url = this.baseUrl.toLowerCase();
|
|
663
|
+
if (url.includes('codestral.mistral.ai')) {
|
|
664
|
+
return ['codestral-2508', 'codestral-latest'];
|
|
665
|
+
}
|
|
666
|
+
if (url.includes('api.mistral.ai')) {
|
|
667
|
+
return [
|
|
668
|
+
'mistral-large-3-25-12',
|
|
669
|
+
'mistral-medium-3-1-25-08',
|
|
670
|
+
'mistral-small-4-0-26-03',
|
|
671
|
+
'mistral-nemo-12b-24-07',
|
|
672
|
+
'codestral-2508',
|
|
673
|
+
'devstral-2-25-12'
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
if (url.includes('api.openai.com')) {
|
|
677
|
+
return [
|
|
678
|
+
'gpt-4o',
|
|
679
|
+
'gpt-4o-mini',
|
|
680
|
+
'gpt-4-turbo',
|
|
681
|
+
'gpt-4',
|
|
682
|
+
'gpt-3.5-turbo'
|
|
683
|
+
];
|
|
684
|
+
}
|
|
685
|
+
if (url.includes('api.groq.com')) {
|
|
686
|
+
return [
|
|
687
|
+
'llama-3.1-70b-versatile',
|
|
688
|
+
'llama-3.1-8b-instant',
|
|
689
|
+
'mixtral-8x7b-32768'
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
// For unknown endpoints, return empty array
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
async generateCompletion(request) {
|
|
696
|
+
const body = {
|
|
697
|
+
model: this.model,
|
|
698
|
+
messages: [
|
|
699
|
+
{ role: 'system', content: request.systemPrompt },
|
|
700
|
+
{ role: 'user', content: request.userPrompt },
|
|
701
|
+
],
|
|
702
|
+
max_tokens: request.maxTokens ?? this.maxOutputTokens,
|
|
703
|
+
temperature: request.temperature ?? 0.3,
|
|
704
|
+
stream: true,
|
|
705
|
+
stream_options: { include_usage: true },
|
|
706
|
+
...(request.stopSequences && { stop: request.stopSequences }),
|
|
707
|
+
};
|
|
708
|
+
if (!this.disableResponseFormat) {
|
|
709
|
+
if (request.responseFormat === 'json' && request.jsonSchema) {
|
|
710
|
+
body.response_format = {
|
|
711
|
+
type: 'json_schema',
|
|
712
|
+
json_schema: {
|
|
713
|
+
name: 'response',
|
|
714
|
+
schema: normalizeOpenAIResponseSchema(request.jsonSchema),
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
else if (request.responseFormat === 'json') {
|
|
719
|
+
body.response_format = { type: 'json_object' };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
723
|
+
method: 'POST',
|
|
724
|
+
headers: {
|
|
725
|
+
'Content-Type': 'application/json',
|
|
726
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
727
|
+
},
|
|
728
|
+
body: JSON.stringify(body),
|
|
729
|
+
});
|
|
730
|
+
if (!response.ok) {
|
|
731
|
+
const error = await response.text();
|
|
732
|
+
const detail = error.trim() || '(empty response body)';
|
|
733
|
+
const err = new Error(`HTTP ${response.status}: ${detail}`);
|
|
734
|
+
err.status = response.status;
|
|
735
|
+
err.retryable = response.status === 429 || response.status >= 500;
|
|
736
|
+
if (response.status === 429) {
|
|
737
|
+
err.retryAfterMs = parseRetryAfterMs(error, response.headers.get('retry-after'));
|
|
738
|
+
}
|
|
739
|
+
throw err;
|
|
740
|
+
}
|
|
741
|
+
let content = '';
|
|
742
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
743
|
+
let finishReason = 'stop';
|
|
744
|
+
let model = this.model;
|
|
745
|
+
const reader = response.body.getReader();
|
|
746
|
+
const decoder = new TextDecoder();
|
|
747
|
+
let buffer = '';
|
|
748
|
+
outer: while (true) {
|
|
749
|
+
const { done, value } = await reader.read();
|
|
750
|
+
if (done)
|
|
751
|
+
break;
|
|
752
|
+
buffer += decoder.decode(value, { stream: true });
|
|
753
|
+
const lines = buffer.split('\n');
|
|
754
|
+
buffer = lines.pop() ?? '';
|
|
755
|
+
for (const line of lines) {
|
|
756
|
+
if (!line.startsWith('data: '))
|
|
757
|
+
continue;
|
|
758
|
+
const data = line.slice(6).trim();
|
|
759
|
+
if (data === '[DONE]')
|
|
760
|
+
break outer;
|
|
761
|
+
try {
|
|
762
|
+
const chunk = JSON.parse(data);
|
|
763
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
764
|
+
if (delta)
|
|
765
|
+
content += delta;
|
|
766
|
+
const fr = chunk.choices?.[0]?.finish_reason;
|
|
767
|
+
if (fr)
|
|
768
|
+
finishReason = fr === 'length' ? 'length' : 'stop';
|
|
769
|
+
if (chunk.model)
|
|
770
|
+
model = chunk.model;
|
|
771
|
+
if (chunk.usage) {
|
|
772
|
+
usage = {
|
|
773
|
+
inputTokens: chunk.usage.prompt_tokens,
|
|
774
|
+
outputTokens: chunk.usage.completion_tokens,
|
|
775
|
+
totalTokens: chunk.usage.total_tokens,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch { /* ignore malformed SSE chunks */ }
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { content, usage, model, finishReason };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// ============================================================================
|
|
786
|
+
// COPILOT PROVIDER (via copilot-api proxy — OpenAI-compatible)
|
|
787
|
+
// ============================================================================
|
|
788
|
+
/**
|
|
789
|
+
* GitHub Copilot provider via copilot-api proxy.
|
|
790
|
+
* Requires a running copilot-api proxy (https://github.com/ericc-ch/copilot-api)
|
|
791
|
+
* which exposes an OpenAI-compatible /v1/chat/completions endpoint.
|
|
792
|
+
*
|
|
793
|
+
* Required env vars:
|
|
794
|
+
* COPILOT_API_BASE_URL — Base URL of the copilot-api proxy (default: http://localhost:4141/v1)
|
|
795
|
+
*
|
|
796
|
+
* Optional env vars:
|
|
797
|
+
* COPILOT_API_KEY — API key if the proxy requires auth (default: "copilot")
|
|
798
|
+
*/
|
|
799
|
+
export class CopilotProvider {
|
|
800
|
+
name = 'copilot';
|
|
801
|
+
maxContextTokens = 128000;
|
|
802
|
+
maxOutputTokens = 4096;
|
|
803
|
+
apiKey;
|
|
804
|
+
model;
|
|
805
|
+
baseUrl;
|
|
806
|
+
constructor(baseUrl, model = DEFAULT_COPILOT_MODEL, apiKey = 'copilot') {
|
|
807
|
+
this.apiKey = apiKey;
|
|
808
|
+
this.baseUrl = normalizeApiBase(baseUrl);
|
|
809
|
+
this.model = model;
|
|
810
|
+
}
|
|
811
|
+
countTokens(text) {
|
|
812
|
+
return estimateTokens(text);
|
|
813
|
+
}
|
|
814
|
+
async generateCompletion(request) {
|
|
815
|
+
const body = {
|
|
816
|
+
model: this.model,
|
|
817
|
+
messages: [
|
|
818
|
+
{ role: 'system', content: request.systemPrompt },
|
|
819
|
+
{ role: 'user', content: request.userPrompt },
|
|
820
|
+
],
|
|
821
|
+
max_tokens: request.maxTokens ?? this.maxOutputTokens,
|
|
822
|
+
temperature: request.temperature ?? 0.3,
|
|
823
|
+
...(request.stopSequences && { stop: request.stopSequences }),
|
|
824
|
+
};
|
|
825
|
+
if (request.responseFormat === 'json' && request.jsonSchema) {
|
|
826
|
+
body.response_format = {
|
|
827
|
+
type: 'json_schema',
|
|
828
|
+
json_schema: {
|
|
829
|
+
name: 'response',
|
|
830
|
+
schema: wrapArraySchema(request.jsonSchema),
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
else if (request.responseFormat === 'json') {
|
|
835
|
+
body.response_format = { type: 'json_object' };
|
|
836
|
+
}
|
|
837
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
838
|
+
method: 'POST',
|
|
839
|
+
headers: {
|
|
840
|
+
'Content-Type': 'application/json',
|
|
841
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
842
|
+
},
|
|
843
|
+
body: JSON.stringify(body),
|
|
844
|
+
});
|
|
845
|
+
if (!response.ok) {
|
|
846
|
+
const error = await response.text();
|
|
847
|
+
const detail = error.trim() || '(empty response body)';
|
|
848
|
+
const err = new Error(`HTTP ${response.status}: ${detail}`);
|
|
849
|
+
err.status = response.status;
|
|
850
|
+
err.retryable = response.status === 429 || response.status >= 500;
|
|
851
|
+
if (response.status === 429) {
|
|
852
|
+
err.retryAfterMs = parseRetryAfterMs(error, response.headers.get('retry-after'));
|
|
853
|
+
}
|
|
854
|
+
throw err;
|
|
855
|
+
}
|
|
856
|
+
const data = await response.json();
|
|
857
|
+
return {
|
|
858
|
+
content: data.choices[0]?.message?.content ?? '',
|
|
859
|
+
usage: {
|
|
860
|
+
inputTokens: data.usage.prompt_tokens,
|
|
861
|
+
outputTokens: data.usage.completion_tokens,
|
|
862
|
+
totalTokens: data.usage.total_tokens,
|
|
863
|
+
},
|
|
864
|
+
model: data.model ?? this.model,
|
|
865
|
+
finishReason: data.choices[0]?.finish_reason === 'stop' ? 'stop' : data.choices[0]?.finish_reason === 'length' ? 'length' : 'error',
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// ============================================================================
|
|
870
|
+
// GEMINI CLI PROVIDER (uses local `gemini` CLI, no API key required)
|
|
871
|
+
// ============================================================================
|
|
872
|
+
/**
|
|
873
|
+
* Gemini CLI provider
|
|
874
|
+
*
|
|
875
|
+
* Routes LLM calls through the local `gemini` CLI binary in non-interactive
|
|
876
|
+
* mode (`gemini -p ...`). Authentication is handled by the Google account
|
|
877
|
+
* session — no GEMINI_API_KEY is required.
|
|
878
|
+
* If the binary is not on PATH, set GEMINI_CLI to its full path.
|
|
879
|
+
*/
|
|
880
|
+
export class GeminiCLIProvider {
|
|
881
|
+
name = 'gemini-cli';
|
|
882
|
+
maxContextTokens = 1_000_000;
|
|
883
|
+
maxOutputTokens = 8_192;
|
|
884
|
+
model;
|
|
885
|
+
constructor(model) {
|
|
886
|
+
this.model = model && model !== 'gemini-cli' ? model : undefined;
|
|
887
|
+
}
|
|
888
|
+
async generateCompletion(request) {
|
|
889
|
+
const { execFileSync } = await import('child_process');
|
|
890
|
+
const fullPrompt = request.systemPrompt
|
|
891
|
+
? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
|
|
892
|
+
: request.userPrompt;
|
|
893
|
+
// gemini CLI: -p for prompt, --output-format json, -m for model
|
|
894
|
+
const args = ['-p', fullPrompt, '--output-format', 'json'];
|
|
895
|
+
if (this.model)
|
|
896
|
+
args.push('-m', this.model);
|
|
897
|
+
const geminiCLIBin = process.env.GEMINI_CLI ?? 'gemini';
|
|
898
|
+
let raw;
|
|
899
|
+
try {
|
|
900
|
+
raw = execFileSync(geminiCLIBin, args, {
|
|
901
|
+
encoding: 'utf8',
|
|
902
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
903
|
+
timeout: 300_000,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
const e = err;
|
|
908
|
+
const detail = e.stderr ?? e.stdout ?? e.message ?? String(err);
|
|
909
|
+
throw Object.assign(new Error(`gemini CLI failed: ${detail}`), { retryable: false });
|
|
910
|
+
}
|
|
911
|
+
// Format: {response: string, stats: {models: {[name]: {tokens: {input, candidates, total}}}}}
|
|
912
|
+
let content = '';
|
|
913
|
+
let inputTokens;
|
|
914
|
+
let outputTokens;
|
|
915
|
+
let modelUsed = this.model ?? 'gemini-cli';
|
|
916
|
+
try {
|
|
917
|
+
const parsed = JSON.parse(raw);
|
|
918
|
+
content = parsed.response ?? '';
|
|
919
|
+
if (parsed.stats?.models) {
|
|
920
|
+
const models = Object.entries(parsed.stats.models);
|
|
921
|
+
if (models.length > 0) {
|
|
922
|
+
modelUsed = models[0][0];
|
|
923
|
+
// Sum tokens across all models used (gemini-cli may use multiple internally)
|
|
924
|
+
inputTokens = models.reduce((sum, [, m]) => sum + (m.tokens?.input ?? 0), 0);
|
|
925
|
+
outputTokens = models.reduce((sum, [, m]) => sum + (m.tokens?.candidates ?? 0), 0);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
content = raw.trim();
|
|
931
|
+
}
|
|
932
|
+
if (!content)
|
|
933
|
+
content = raw.trim();
|
|
934
|
+
inputTokens ??= estimateTokens(fullPrompt);
|
|
935
|
+
outputTokens ??= estimateTokens(content);
|
|
936
|
+
return {
|
|
937
|
+
content,
|
|
938
|
+
usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
939
|
+
model: modelUsed,
|
|
940
|
+
finishReason: 'stop',
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
countTokens(text) {
|
|
944
|
+
return estimateTokens(text);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// ============================================================================
|
|
948
|
+
// CURSOR AGENT CLI PROVIDER (uses local `cursor-agent` CLI, no cloud API key)
|
|
949
|
+
// ============================================================================
|
|
950
|
+
/**
|
|
951
|
+
* Cursor Agent CLI provider
|
|
952
|
+
*
|
|
953
|
+
* Routes LLM calls through the Cursor Agent CLI in print mode (`-p`, JSON output).
|
|
954
|
+
* Authentication is handled by Cursor (see Cursor CLI headless documentation) —
|
|
955
|
+
* e.g. `cursor auth login` or `CURSOR_API_KEY` — not ANTHROPIC_API_KEY / OPENAI_API_KEY.
|
|
956
|
+
* If the binary is not on PATH, set `CURSOR_AGENT_CLI` to its full path.
|
|
957
|
+
*/
|
|
958
|
+
export class CursorAgentProvider {
|
|
959
|
+
name = 'cursor-agent';
|
|
960
|
+
maxContextTokens = 1_000_000;
|
|
961
|
+
maxOutputTokens = 8192;
|
|
962
|
+
model;
|
|
963
|
+
constructor(model) {
|
|
964
|
+
this.model = model && model !== 'cursor-agent' ? model : undefined;
|
|
965
|
+
}
|
|
966
|
+
async generateCompletion(request) {
|
|
967
|
+
const { execFileSync } = await import('child_process');
|
|
968
|
+
const fullPrompt = request.systemPrompt
|
|
969
|
+
? `${request.systemPrompt}\n\n---\n\n${request.userPrompt}`
|
|
970
|
+
: request.userPrompt;
|
|
971
|
+
const args = ['-p', fullPrompt, '--output-format', 'json'];
|
|
972
|
+
if (this.model)
|
|
973
|
+
args.push('--model', this.model);
|
|
974
|
+
const bin = process.env.CURSOR_AGENT_CLI ?? 'cursor-agent';
|
|
975
|
+
let raw;
|
|
976
|
+
try {
|
|
977
|
+
raw = execFileSync(bin, args, {
|
|
978
|
+
encoding: 'utf8',
|
|
979
|
+
maxBuffer: LLM_CLI_MAX_BUFFER_BYTES,
|
|
980
|
+
timeout: LLM_CLI_TIMEOUT_MS,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
catch (err) {
|
|
984
|
+
const e = err;
|
|
985
|
+
const detail = e.stderr ?? e.stdout ?? e.message ?? String(err);
|
|
986
|
+
throw Object.assign(new Error(`cursor-agent CLI failed: ${detail}`), { retryable: false });
|
|
987
|
+
}
|
|
988
|
+
let content = '';
|
|
989
|
+
let inputTokens;
|
|
990
|
+
let outputTokens;
|
|
991
|
+
try {
|
|
992
|
+
const parsed = JSON.parse(raw);
|
|
993
|
+
if (parsed.is_error === true && typeof parsed.result === 'string') {
|
|
994
|
+
throw Object.assign(new Error(`cursor-agent CLI error: ${parsed.result}`), { retryable: false });
|
|
995
|
+
}
|
|
996
|
+
if (typeof parsed.result === 'string') {
|
|
997
|
+
content = parsed.result;
|
|
998
|
+
}
|
|
999
|
+
else if (typeof parsed.response === 'string') {
|
|
1000
|
+
content = parsed.response;
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
content = String(parsed.message ?? parsed.text ?? parsed.content ?? '');
|
|
1004
|
+
}
|
|
1005
|
+
const u = parsed.usage;
|
|
1006
|
+
if (u) {
|
|
1007
|
+
inputTokens = (u.input_tokens ?? u.inputTokens);
|
|
1008
|
+
outputTokens = (u.output_tokens ?? u.outputTokens);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch (err) {
|
|
1012
|
+
if (err instanceof Error && /cursor-agent CLI error:/.test(err.message))
|
|
1013
|
+
throw err;
|
|
1014
|
+
content = raw.trim();
|
|
1015
|
+
}
|
|
1016
|
+
if (!content)
|
|
1017
|
+
content = raw.trim();
|
|
1018
|
+
inputTokens ??= estimateTokens(fullPrompt);
|
|
1019
|
+
outputTokens ??= estimateTokens(content);
|
|
1020
|
+
return {
|
|
1021
|
+
content,
|
|
1022
|
+
usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
1023
|
+
model: this.model ?? 'cursor-agent',
|
|
1024
|
+
finishReason: 'stop',
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
countTokens(text) {
|
|
1028
|
+
return estimateTokens(text);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// ============================================================================
|
|
1032
|
+
// GEMINI PROVIDER
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
/**
|
|
1035
|
+
* Google Gemini provider
|
|
1036
|
+
*/
|
|
1037
|
+
export class GeminiProvider {
|
|
1038
|
+
name = 'gemini';
|
|
1039
|
+
maxContextTokens = 1000000;
|
|
1040
|
+
maxOutputTokens = 8192;
|
|
1041
|
+
apiKey;
|
|
1042
|
+
model;
|
|
1043
|
+
baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
1044
|
+
constructor(apiKey, model = DEFAULT_GEMINI_MODEL) {
|
|
1045
|
+
this.apiKey = apiKey;
|
|
1046
|
+
this.model = model;
|
|
1047
|
+
}
|
|
1048
|
+
countTokens(text) {
|
|
1049
|
+
return estimateTokens(text);
|
|
1050
|
+
}
|
|
1051
|
+
async generateCompletion(request) {
|
|
1052
|
+
const body = {
|
|
1053
|
+
contents: [
|
|
1054
|
+
{ role: 'user', parts: [{ text: request.userPrompt }] },
|
|
1055
|
+
],
|
|
1056
|
+
systemInstruction: {
|
|
1057
|
+
parts: [{ text: request.systemPrompt }],
|
|
1058
|
+
},
|
|
1059
|
+
generationConfig: {
|
|
1060
|
+
temperature: request.temperature ?? 0.3,
|
|
1061
|
+
maxOutputTokens: request.maxTokens ?? this.maxOutputTokens,
|
|
1062
|
+
...(request.responseFormat === 'json' && { responseMimeType: 'application/json' }),
|
|
1063
|
+
...(request.responseFormat === 'json' && request.jsonSchema && { responseSchema: request.jsonSchema }),
|
|
1064
|
+
...(request.stopSequences && { stopSequences: request.stopSequences }),
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
const url = `${this.baseUrl}/${this.model}:generateContent?key=${this.apiKey}`;
|
|
1068
|
+
const response = await fetch(url, {
|
|
1069
|
+
method: 'POST',
|
|
1070
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1071
|
+
body: JSON.stringify(body),
|
|
1072
|
+
});
|
|
1073
|
+
if (!response.ok) {
|
|
1074
|
+
const error = await response.text();
|
|
1075
|
+
const detail = error.trim() || '(empty response body)';
|
|
1076
|
+
const err = new Error(`HTTP ${response.status}: ${detail}`);
|
|
1077
|
+
err.status = response.status;
|
|
1078
|
+
err.retryable = response.status === 429 || response.status >= 500;
|
|
1079
|
+
if (response.status === 429) {
|
|
1080
|
+
err.retryAfterMs = parseRetryAfterMs(error, response.headers.get('retry-after'));
|
|
1081
|
+
}
|
|
1082
|
+
throw err;
|
|
1083
|
+
}
|
|
1084
|
+
const data = await response.json();
|
|
1085
|
+
const content = data.candidates[0]?.content?.parts?.map(p => p.text).join('') ?? '';
|
|
1086
|
+
const finishReason = data.candidates[0]?.finishReason;
|
|
1087
|
+
return {
|
|
1088
|
+
content,
|
|
1089
|
+
usage: {
|
|
1090
|
+
inputTokens: data.usageMetadata.promptTokenCount,
|
|
1091
|
+
outputTokens: data.usageMetadata.candidatesTokenCount,
|
|
1092
|
+
totalTokens: data.usageMetadata.totalTokenCount,
|
|
1093
|
+
},
|
|
1094
|
+
model: this.model,
|
|
1095
|
+
finishReason: finishReason === 'STOP' ? 'stop' : finishReason === 'MAX_TOKENS' ? 'length' : 'error',
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// ============================================================================
|
|
1100
|
+
// MOCK PROVIDER (for testing)
|
|
1101
|
+
// ============================================================================
|
|
1102
|
+
/**
|
|
1103
|
+
* Mock provider for testing
|
|
1104
|
+
*/
|
|
1105
|
+
export class MockLLMProvider {
|
|
1106
|
+
name = 'mock';
|
|
1107
|
+
maxContextTokens = 100000;
|
|
1108
|
+
maxOutputTokens = 4096;
|
|
1109
|
+
responses = new Map();
|
|
1110
|
+
defaultResponse = '{"result": "mock response"}';
|
|
1111
|
+
callHistory = [];
|
|
1112
|
+
shouldFail = false;
|
|
1113
|
+
failCount = 0;
|
|
1114
|
+
currentFailCount = 0;
|
|
1115
|
+
setResponse(promptContains, response) {
|
|
1116
|
+
this.responses.set(promptContains, response);
|
|
1117
|
+
}
|
|
1118
|
+
setDefaultResponse(response) {
|
|
1119
|
+
this.defaultResponse = response;
|
|
1120
|
+
}
|
|
1121
|
+
countTokens(text) {
|
|
1122
|
+
return estimateTokens(text);
|
|
1123
|
+
}
|
|
1124
|
+
async generateCompletion(request) {
|
|
1125
|
+
this.callHistory.push(request);
|
|
1126
|
+
if (this.shouldFail && this.currentFailCount < this.failCount) {
|
|
1127
|
+
this.currentFailCount++;
|
|
1128
|
+
const err = new Error('Mock failure');
|
|
1129
|
+
err.status = 500;
|
|
1130
|
+
err.retryable = true;
|
|
1131
|
+
throw err;
|
|
1132
|
+
}
|
|
1133
|
+
// Find matching response
|
|
1134
|
+
let content = this.defaultResponse;
|
|
1135
|
+
for (const [key, value] of this.responses) {
|
|
1136
|
+
if (request.userPrompt.includes(key) || request.systemPrompt.includes(key)) {
|
|
1137
|
+
content = value;
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const inputTokens = this.countTokens(request.systemPrompt + request.userPrompt);
|
|
1142
|
+
const outputTokens = this.countTokens(content);
|
|
1143
|
+
return {
|
|
1144
|
+
content,
|
|
1145
|
+
usage: {
|
|
1146
|
+
inputTokens,
|
|
1147
|
+
outputTokens,
|
|
1148
|
+
totalTokens: inputTokens + outputTokens,
|
|
1149
|
+
},
|
|
1150
|
+
model: 'mock-model',
|
|
1151
|
+
finishReason: 'stop',
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
reset() {
|
|
1155
|
+
this.callHistory = [];
|
|
1156
|
+
this.shouldFail = false;
|
|
1157
|
+
this.failCount = 0;
|
|
1158
|
+
this.currentFailCount = 0;
|
|
1159
|
+
this.responses.clear();
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// ============================================================================
|
|
1163
|
+
// LLM SERVICE
|
|
1164
|
+
// ============================================================================
|
|
1165
|
+
/**
|
|
1166
|
+
* LLM Service - main interface for LLM interactions
|
|
1167
|
+
*/
|
|
1168
|
+
export class LLMService {
|
|
1169
|
+
provider;
|
|
1170
|
+
retryConfig;
|
|
1171
|
+
options;
|
|
1172
|
+
tokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, requests: 0 };
|
|
1173
|
+
costTracking = { estimatedCost: 0, currency: 'USD', byProvider: {} };
|
|
1174
|
+
requestLog = [];
|
|
1175
|
+
constructor(provider, options = {}) {
|
|
1176
|
+
this.provider = provider;
|
|
1177
|
+
this.options = {
|
|
1178
|
+
provider: options.provider ?? 'anthropic',
|
|
1179
|
+
model: options.model ?? '',
|
|
1180
|
+
apiBase: options.apiBase ?? '',
|
|
1181
|
+
sslVerify: options.sslVerify ?? true,
|
|
1182
|
+
openaiCompatBaseUrl: options.openaiCompatBaseUrl ?? '',
|
|
1183
|
+
maxRetries: options.maxRetries ?? DEFAULT_LLM_MAX_RETRIES,
|
|
1184
|
+
initialDelay: options.initialDelay ?? DEFAULT_LLM_INITIAL_DELAY_MS,
|
|
1185
|
+
maxDelay: options.maxDelay ?? DEFAULT_LLM_MAX_DELAY_MS,
|
|
1186
|
+
timeout: options.timeout ?? DEFAULT_LLM_TIMEOUT_MS,
|
|
1187
|
+
costWarningThreshold: options.costWarningThreshold ?? DEFAULT_LLM_COST_WARNING_THRESHOLD,
|
|
1188
|
+
logDir: options.logDir ?? `${OPENLORE_DIR}/${OPENLORE_LOGS_SUBDIR}`,
|
|
1189
|
+
enableLogging: options.enableLogging ?? false,
|
|
1190
|
+
disableResponseFormat: options.disableResponseFormat ?? false,
|
|
1191
|
+
};
|
|
1192
|
+
this.retryConfig = {
|
|
1193
|
+
maxRetries: this.options.maxRetries,
|
|
1194
|
+
initialDelay: this.options.initialDelay,
|
|
1195
|
+
maxDelay: this.options.maxDelay,
|
|
1196
|
+
timeout: this.options.timeout,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Get the provider name
|
|
1201
|
+
*/
|
|
1202
|
+
getProviderName() {
|
|
1203
|
+
return this.provider.name;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Get maximum context tokens for the provider
|
|
1207
|
+
*/
|
|
1208
|
+
getMaxContextTokens() {
|
|
1209
|
+
return this.provider.maxContextTokens;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Count tokens in text
|
|
1213
|
+
*/
|
|
1214
|
+
countTokens(text) {
|
|
1215
|
+
return this.provider.countTokens(text);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Get current token usage
|
|
1219
|
+
*/
|
|
1220
|
+
getTokenUsage() {
|
|
1221
|
+
return { ...this.tokenUsage };
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Get current cost tracking
|
|
1225
|
+
*/
|
|
1226
|
+
getCostTracking() {
|
|
1227
|
+
return { ...this.costTracking };
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Reset usage tracking
|
|
1231
|
+
*/
|
|
1232
|
+
resetTracking() {
|
|
1233
|
+
this.tokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, requests: 0 };
|
|
1234
|
+
this.costTracking = { estimatedCost: 0, currency: 'USD', byProvider: {} };
|
|
1235
|
+
this.requestLog = [];
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Generate a completion with retry logic
|
|
1239
|
+
*/
|
|
1240
|
+
async complete(request) {
|
|
1241
|
+
// Pre-calculate tokens and warn if approaching limit
|
|
1242
|
+
const inputTokens = this.countTokens(request.systemPrompt + request.userPrompt);
|
|
1243
|
+
const maxTokens = request.maxTokens ?? this.provider.maxOutputTokens;
|
|
1244
|
+
const totalExpected = inputTokens + maxTokens;
|
|
1245
|
+
if (totalExpected > this.provider.maxContextTokens * CONTEXT_LIMIT_WARNING_RATIO) {
|
|
1246
|
+
logger.warning(`Approaching context limit: ${totalExpected} tokens (max: ${this.provider.maxContextTokens})`);
|
|
1247
|
+
}
|
|
1248
|
+
if (totalExpected > this.provider.maxContextTokens) {
|
|
1249
|
+
throw new Error(`Request exceeds context limit: ${totalExpected} > ${this.provider.maxContextTokens}`);
|
|
1250
|
+
}
|
|
1251
|
+
// Execute with retry logic
|
|
1252
|
+
let lastError = null;
|
|
1253
|
+
let delay = this.retryConfig.initialDelay;
|
|
1254
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
1255
|
+
try {
|
|
1256
|
+
logger.debug(`LLM request attempt ${attempt + 1}/${this.retryConfig.maxRetries + 1}`);
|
|
1257
|
+
const response = await this.executeWithTimeout(request);
|
|
1258
|
+
// Update tracking
|
|
1259
|
+
this.updateTracking(response);
|
|
1260
|
+
// Log if enabled
|
|
1261
|
+
if (this.options.enableLogging) {
|
|
1262
|
+
this.logRequest(request, response);
|
|
1263
|
+
}
|
|
1264
|
+
// Check cost threshold
|
|
1265
|
+
if (this.costTracking.estimatedCost > this.options.costWarningThreshold) {
|
|
1266
|
+
logger.warning(`Cost threshold exceeded: $${this.costTracking.estimatedCost.toFixed(4)} > $${this.options.costWarningThreshold}`);
|
|
1267
|
+
}
|
|
1268
|
+
return response;
|
|
1269
|
+
}
|
|
1270
|
+
catch (error) {
|
|
1271
|
+
lastError = error;
|
|
1272
|
+
const errWithStatus = error;
|
|
1273
|
+
// Log error
|
|
1274
|
+
if (this.options.enableLogging) {
|
|
1275
|
+
this.logRequest(request, undefined, lastError.message);
|
|
1276
|
+
}
|
|
1277
|
+
// Check if retryable
|
|
1278
|
+
if (!errWithStatus.retryable || attempt === this.retryConfig.maxRetries) {
|
|
1279
|
+
throw lastError;
|
|
1280
|
+
}
|
|
1281
|
+
// Use the provider-supplied reset time if available, otherwise exponential backoff
|
|
1282
|
+
const retryAfterMs = errWithStatus.retryAfterMs;
|
|
1283
|
+
const waitMs = retryAfterMs !== undefined ? retryAfterMs : delay;
|
|
1284
|
+
logger.warning(`LLM request failed (attempt ${attempt + 1}), retrying in ${waitMs}ms: ${lastError.message}`);
|
|
1285
|
+
await this.sleep(waitMs);
|
|
1286
|
+
// Only advance the backoff delay when we didn't use a provider-supplied wait
|
|
1287
|
+
if (retryAfterMs === undefined) {
|
|
1288
|
+
delay = Math.min(delay * 2, this.retryConfig.maxDelay);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
throw lastError ?? new Error('Unknown error');
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Generate a completion expecting JSON response
|
|
1296
|
+
*/
|
|
1297
|
+
async completeJSON(request, schema) {
|
|
1298
|
+
const jsonRequest = { ...request, responseFormat: 'json', jsonSchema: schema };
|
|
1299
|
+
// Add JSON instruction to prompt if not already present
|
|
1300
|
+
if (!jsonRequest.systemPrompt.toLowerCase().includes('json')) {
|
|
1301
|
+
jsonRequest.systemPrompt += '\n\nRespond with valid JSON only.';
|
|
1302
|
+
}
|
|
1303
|
+
// When a schema is provided, append it to the system prompt so the model
|
|
1304
|
+
// knows the exact shape expected (especially that it must start an array).
|
|
1305
|
+
// This addresses issue #26: without this, models may return a single object.
|
|
1306
|
+
if (schema) {
|
|
1307
|
+
jsonRequest.systemPrompt += `\n\nYour response MUST conform to this JSON Schema:\n${JSON.stringify(schema)}`;
|
|
1308
|
+
}
|
|
1309
|
+
const response = await this.complete(jsonRequest);
|
|
1310
|
+
let content = response.content;
|
|
1311
|
+
// Extract JSON from markdown code blocks if present
|
|
1312
|
+
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1313
|
+
if (jsonMatch) {
|
|
1314
|
+
content = jsonMatch[1].trim();
|
|
1315
|
+
}
|
|
1316
|
+
// Parse JSON
|
|
1317
|
+
let parsed;
|
|
1318
|
+
try {
|
|
1319
|
+
parsed = JSON.parse(content);
|
|
1320
|
+
}
|
|
1321
|
+
catch (parseError) {
|
|
1322
|
+
// Retry with correction prompt for parse errors
|
|
1323
|
+
logger.warning('JSON parse failed, attempting correction');
|
|
1324
|
+
const correctionRequest = {
|
|
1325
|
+
systemPrompt: 'Fix the following invalid JSON and return only valid JSON. Do not include any explanation.',
|
|
1326
|
+
userPrompt: `Invalid JSON:\n${content}\n\nError: ${parseError.message}\n\nReturn the corrected JSON:`,
|
|
1327
|
+
temperature: 0.1,
|
|
1328
|
+
responseFormat: 'json',
|
|
1329
|
+
};
|
|
1330
|
+
const correctionResponse = await this.complete(correctionRequest);
|
|
1331
|
+
let correctedContent = correctionResponse.content;
|
|
1332
|
+
// Extract from code blocks again
|
|
1333
|
+
const correctedMatch = correctedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1334
|
+
if (correctedMatch) {
|
|
1335
|
+
correctedContent = correctedMatch[1].trim();
|
|
1336
|
+
}
|
|
1337
|
+
parsed = JSON.parse(correctedContent);
|
|
1338
|
+
}
|
|
1339
|
+
// Unwrap single-key object whose value is an array (e.g. {entities:[...]} → [...])
|
|
1340
|
+
// LLM correction attempts sometimes wrap arrays in an object
|
|
1341
|
+
if (parsed !== null &&
|
|
1342
|
+
typeof parsed === 'object' &&
|
|
1343
|
+
!Array.isArray(parsed)) {
|
|
1344
|
+
const keys = Object.keys(parsed);
|
|
1345
|
+
if (keys.length === 1) {
|
|
1346
|
+
const val = parsed[keys[0]];
|
|
1347
|
+
if (Array.isArray(val)) {
|
|
1348
|
+
parsed = val;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
// Validate against schema if provided (after successful parsing)
|
|
1353
|
+
if (schema) {
|
|
1354
|
+
this.validateSchema(parsed, schema);
|
|
1355
|
+
}
|
|
1356
|
+
return parsed;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Execute request with timeout
|
|
1360
|
+
*/
|
|
1361
|
+
async executeWithTimeout(request) {
|
|
1362
|
+
const timeoutMs = this.retryConfig.timeout;
|
|
1363
|
+
const result = await Promise.race([
|
|
1364
|
+
this.provider.generateCompletion(request),
|
|
1365
|
+
new Promise((_, reject) => {
|
|
1366
|
+
setTimeout(() => reject(new Error(`LLM request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
1367
|
+
}),
|
|
1368
|
+
]);
|
|
1369
|
+
return result;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Update tracking after a successful request
|
|
1373
|
+
*/
|
|
1374
|
+
updateTracking(response) {
|
|
1375
|
+
this.tokenUsage.inputTokens += response.usage.inputTokens;
|
|
1376
|
+
this.tokenUsage.outputTokens += response.usage.outputTokens;
|
|
1377
|
+
this.tokenUsage.totalTokens += response.usage.totalTokens;
|
|
1378
|
+
this.tokenUsage.requests++;
|
|
1379
|
+
// Calculate cost
|
|
1380
|
+
const cost = this.calculateCost(response);
|
|
1381
|
+
this.costTracking.estimatedCost += cost;
|
|
1382
|
+
this.costTracking.byProvider[this.provider.name] = (this.costTracking.byProvider[this.provider.name] ?? 0) + cost;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Calculate cost for a response
|
|
1386
|
+
*/
|
|
1387
|
+
calculateCost(response) {
|
|
1388
|
+
const modelPricing = lookupPricing(this.provider.name, response.model);
|
|
1389
|
+
const inputCost = (response.usage.inputTokens / 1_000_000) * modelPricing.input;
|
|
1390
|
+
const outputCost = (response.usage.outputTokens / 1_000_000) * modelPricing.output;
|
|
1391
|
+
return inputCost + outputCost;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Log request/response
|
|
1395
|
+
*/
|
|
1396
|
+
logRequest(request, response, error) {
|
|
1397
|
+
const logEntry = {
|
|
1398
|
+
timestamp: new Date().toISOString(),
|
|
1399
|
+
request: this.redactSecrets(request),
|
|
1400
|
+
response,
|
|
1401
|
+
error,
|
|
1402
|
+
};
|
|
1403
|
+
this.requestLog.push(logEntry);
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Redact potential secrets from request
|
|
1407
|
+
*/
|
|
1408
|
+
redactSecrets(request) {
|
|
1409
|
+
const secretPatterns = [
|
|
1410
|
+
/(?:api[_-]?key|password|secret|token|auth)['":\s]*[=:]\s*['"]?[\w-]{20,}['"]?/gi,
|
|
1411
|
+
/['"]?[a-zA-Z0-9]{32,}['"]?/g, // Long alphanumeric strings
|
|
1412
|
+
];
|
|
1413
|
+
let systemPrompt = request.systemPrompt;
|
|
1414
|
+
let userPrompt = request.userPrompt;
|
|
1415
|
+
for (const pattern of secretPatterns) {
|
|
1416
|
+
systemPrompt = systemPrompt.replace(pattern, '[REDACTED]');
|
|
1417
|
+
userPrompt = userPrompt.replace(pattern, '[REDACTED]');
|
|
1418
|
+
}
|
|
1419
|
+
return { ...request, systemPrompt, userPrompt };
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Simple schema validation
|
|
1423
|
+
*/
|
|
1424
|
+
validateSchema(data, schema) {
|
|
1425
|
+
const schemaObj = schema;
|
|
1426
|
+
if (schemaObj.type === 'array') {
|
|
1427
|
+
if (!Array.isArray(data)) {
|
|
1428
|
+
throw new Error('Expected JSON array but received object');
|
|
1429
|
+
}
|
|
1430
|
+
// Validate each item against the items schema if provided
|
|
1431
|
+
const itemsSchema = schemaObj.items;
|
|
1432
|
+
if (itemsSchema?.required && Array.isArray(itemsSchema.required)) {
|
|
1433
|
+
for (const item of data) {
|
|
1434
|
+
for (const field of itemsSchema.required) {
|
|
1435
|
+
if (!(field in item)) {
|
|
1436
|
+
throw new Error(`Missing required field in array item: ${field}`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
else if (schemaObj.type === 'object' && schemaObj.required && Array.isArray(schemaObj.required)) {
|
|
1443
|
+
const dataObj = data;
|
|
1444
|
+
for (const field of schemaObj.required) {
|
|
1445
|
+
if (!(field in dataObj)) {
|
|
1446
|
+
throw new Error(`Missing required field: ${field}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Save logs to disk
|
|
1453
|
+
*/
|
|
1454
|
+
async saveLogs() {
|
|
1455
|
+
if (this.requestLog.length === 0)
|
|
1456
|
+
return;
|
|
1457
|
+
await mkdir(this.options.logDir, { recursive: true });
|
|
1458
|
+
const filename = `llm-log-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
|
1459
|
+
const filepath = join(this.options.logDir, filename);
|
|
1460
|
+
await writeFile(filepath, JSON.stringify({
|
|
1461
|
+
summary: {
|
|
1462
|
+
tokenUsage: this.tokenUsage,
|
|
1463
|
+
costTracking: this.costTracking,
|
|
1464
|
+
},
|
|
1465
|
+
requests: this.requestLog,
|
|
1466
|
+
}, null, 2));
|
|
1467
|
+
logger.debug(`Saved LLM logs to ${filepath}`);
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Sleep helper
|
|
1471
|
+
*/
|
|
1472
|
+
sleep(ms) {
|
|
1473
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
// ============================================================================
|
|
1477
|
+
// FACTORY FUNCTIONS
|
|
1478
|
+
// ============================================================================
|
|
1479
|
+
/**
|
|
1480
|
+
* Create an LLM service with the specified provider
|
|
1481
|
+
*/
|
|
1482
|
+
export function createLLMService(options = {}) {
|
|
1483
|
+
const providerName = options.provider ?? 'anthropic';
|
|
1484
|
+
const sslVerify = options.sslVerify ?? true;
|
|
1485
|
+
let provider;
|
|
1486
|
+
if (providerName === 'anthropic') {
|
|
1487
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1488
|
+
if (!apiKey) {
|
|
1489
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is not set');
|
|
1490
|
+
}
|
|
1491
|
+
const apiBase = options.apiBase ?? process.env.ANTHROPIC_API_BASE ?? undefined;
|
|
1492
|
+
provider = new AnthropicProvider(apiKey, options.model ?? DEFAULT_ANTHROPIC_MODEL, apiBase, sslVerify);
|
|
1493
|
+
}
|
|
1494
|
+
else if (providerName === 'openai') {
|
|
1495
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
1496
|
+
if (!apiKey) {
|
|
1497
|
+
throw new Error('OPENAI_API_KEY environment variable is not set');
|
|
1498
|
+
}
|
|
1499
|
+
const apiBase = options.apiBase ?? process.env.OPENAI_API_BASE ?? undefined;
|
|
1500
|
+
provider = new OpenAIProvider(apiKey, options.model ?? DEFAULT_OPENAI_MODEL, apiBase, sslVerify);
|
|
1501
|
+
}
|
|
1502
|
+
else if (providerName === 'openai-compat') {
|
|
1503
|
+
const apiKey = process.env.OPENAI_COMPAT_API_KEY;
|
|
1504
|
+
const baseUrl = options.openaiCompatBaseUrl ?? options.apiBase ?? process.env.OPENAI_COMPAT_BASE_URL;
|
|
1505
|
+
if (!apiKey) {
|
|
1506
|
+
throw new Error('OPENAI_COMPAT_API_KEY environment variable is not set');
|
|
1507
|
+
}
|
|
1508
|
+
if (!baseUrl) {
|
|
1509
|
+
throw new Error('openaiCompatBaseUrl must be set in config or OPENAI_COMPAT_BASE_URL env var (e.g. https://api.mistral.ai/v1)');
|
|
1510
|
+
}
|
|
1511
|
+
provider = new OpenAICompatibleProvider(apiKey, baseUrl, options.model ?? DEFAULT_OPENAI_COMPAT_MODEL, options.disableResponseFormat ?? false);
|
|
1512
|
+
}
|
|
1513
|
+
else if (providerName === 'copilot') {
|
|
1514
|
+
const baseUrl = options.openaiCompatBaseUrl ?? options.apiBase ?? process.env.COPILOT_API_BASE_URL ?? 'http://localhost:4141/v1';
|
|
1515
|
+
const apiKey = process.env.COPILOT_API_KEY ?? 'copilot';
|
|
1516
|
+
provider = new CopilotProvider(baseUrl, options.model ?? DEFAULT_COPILOT_MODEL, apiKey);
|
|
1517
|
+
}
|
|
1518
|
+
else if (providerName === 'gemini') {
|
|
1519
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
1520
|
+
if (!apiKey) {
|
|
1521
|
+
throw new Error('GEMINI_API_KEY environment variable is not set');
|
|
1522
|
+
}
|
|
1523
|
+
provider = new GeminiProvider(apiKey, options.model ?? DEFAULT_GEMINI_MODEL);
|
|
1524
|
+
}
|
|
1525
|
+
else if (providerName === 'claude-code') {
|
|
1526
|
+
provider = new ClaudeCodeProvider(options.model);
|
|
1527
|
+
}
|
|
1528
|
+
else if (providerName === 'mistral-vibe') {
|
|
1529
|
+
provider = new MistralVibeProvider(options.model);
|
|
1530
|
+
}
|
|
1531
|
+
else if (providerName === 'gemini-cli') {
|
|
1532
|
+
provider = new GeminiCLIProvider(options.model);
|
|
1533
|
+
}
|
|
1534
|
+
else if (providerName === 'cursor-agent') {
|
|
1535
|
+
provider = new CursorAgentProvider(options.model);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
throw new Error(`Unknown provider: ${providerName}. Supported: anthropic, openai, openai-compat, copilot, gemini, gemini-cli, claude-code, mistral-vibe, cursor-agent`);
|
|
1539
|
+
}
|
|
1540
|
+
if (!sslVerify) {
|
|
1541
|
+
logger.warning('SSL verification is disabled. Use only for trusted internal servers.');
|
|
1542
|
+
}
|
|
1543
|
+
return new LLMService(provider, options);
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Create an LLM service with a mock provider (for testing)
|
|
1547
|
+
*/
|
|
1548
|
+
export function createMockLLMService(options = {}) {
|
|
1549
|
+
const provider = new MockLLMProvider();
|
|
1550
|
+
const service = new LLMService(provider, options);
|
|
1551
|
+
return { service, provider };
|
|
1552
|
+
}
|
|
1553
|
+
//# sourceMappingURL=llm-service.js.map
|