Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e714a13ff | |||
| a6cd5413d8 | |||
| 9c03dfa717 | |||
| c5601d8d7d | |||
| a281aea0e7 | |||
| e8bc6b9794 | |||
| e018199fef | |||
| 35ecf3e551 | |||
| ee028033c7 | |||
| 7adfcabfa0 | |||
| 51201b00be | |||
| 0d43d014eb | |||
| df7d5062bc | |||
| 98aec6c9d3 | |||
| 5a65b12a43 | |||
| 741bea5af8 | |||
| ad1e35a425 | |||
| 2e2239b138 | |||
| 03aac0cd80 | |||
| 22cc73f614 | |||
| 06b05ddfe9 | |||
| 2670a4a171 | |||
| 1699c9a63a | |||
| 808dd42ae9 | |||
| 71af56f493 | |||
| 1167b4f3a6 | |||
| e8d6370b0c | |||
| e243376a39 | |||
| 4f1be9d512 | |||
| 94d36ce719 | |||
| 5df7498f03 | |||
| c2b87252ac | |||
| be353bb3da | |||
| 0a7791de1f | |||
| 7cca102e14 | |||
| 4f591de6a9 | |||
| 60436b3ddf | |||
| df5954052d | |||
| a66a2784c3 | |||
| 47031cea25 | |||
| e29740479e | |||
| 74f607ff8e | |||
| 0edf6d777b | |||
| de630c64d4 | |||
| 3ff63562c8 | |||
| 4d6f908fbd | |||
| 6521fdcc94 | |||
| 1393eace27 | |||
| eebb99c1e3 | |||
| 9ed44f7a83 | |||
| 6a95cc5f19 | |||
| 1d0637f39c | |||
| f2c16bae7a | |||
| 204f15ce1c | |||
| 8118e11596 | |||
| f7b593df35 | |||
| 2e0358a7a1 | |||
| 0fbc10f807 | |||
| c093ca1760 | |||
| dc85299a73 | |||
| 4533ed3ea8 | |||
| 66d7664d1b | |||
| 50aeb7098f | |||
| c2e6eafb45 | |||
| 0220628c39 | |||
| 5052220195 | |||
| abf3081e56 | |||
| 125cb78a31 | |||
| 17539228ea | |||
| bdb7cc36fa | |||
| d3e1065707 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add claude 4.5 haiku
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: finalize document content during approval flow
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Expose --version in cline cli command
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: prevent duplicate diff errors when parallel tool calling is enabled
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Log Persistence errors to PostHog
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat: add OpenAI Codex (ChatGPT Plus/Pro) provider
|
||||
|
||||
Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
|
||||
|
||||
Models available:
|
||||
- gpt-5.2-codex (default)
|
||||
- gpt-5.1-codex-max
|
||||
- gpt-5.1-codex-mini
|
||||
- gpt-5.2
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix two bugs in DiffViewProvider file editing:
|
||||
|
||||
1. **Line boundary validation**: Add `safelyTruncateDocument()` to prevent out-of-bounds line errors on JetBrains hosts (fixes #8423, #8429). The gRPC protocol strictly validates line numbers, causing "truncateDocument INTERNAL: Wrong line" errors when `truncateDocument()` was called with a line number >= document line count.
|
||||
|
||||
2. **Content concatenation on final update**: When replacing content without a trailing newline, the old content at line N+1 was concatenated to the new content. Fixed by extending the replacement range to cover the entire document on final update.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
docs: fix outdated Ollama model names in documentation
|
||||
|
||||
Updated recommended Ollama models to use correct identifiers:
|
||||
- Changed qwen3-coder-30b to qwen2.5-coder:32b
|
||||
- Changed devstral-small to codellama:34b-code
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
OpenAI GPT-5 Codex models are now using Apply Patch tool for diff edits.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue where tool call ids are invalid when switching between models using the chat completion format and the responses api format.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: improve Jupyter notebook diff view and reduce LLM context for notebook edits
|
||||
|
||||
- Restore switchToSpecializedEditor() for Jupyter notebook diff views that was accidentally removed during rebase
|
||||
- Open .ipynb files in Jupyter notebook editor after save instead of leaving stale diff view
|
||||
- Strip notebook outputs from content sent to LLM, reducing context by 95% (196KB → 9KB)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: add chat output on skill use
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding telemetry for background exec terminal
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
This pull request introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness. The feature allows users to seamlessly work with Jupyter notebooks using Cline's AI capabilities while preserving the notebook's JSON structure.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: removes retry message from UI after retry succeeds
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Support native tool calling for LM Studio and Ollama provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fixing integration tests from testing framework
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add claude 4.5 opus into sap provider.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Limite Vertex and LiteLLM options when they're remote configured
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix crash when the Context Menu has a type but no options
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Throttle the remote config fetch
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve history view filter menu
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add git worktree management UI for running parallel Cline sessions
|
||||
@@ -16,13 +16,6 @@
|
||||
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
|
||||
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
|
||||
|
||||
# ============================================================================
|
||||
# TELEMETRY PROVIDER CONTROL
|
||||
# ============================================================================
|
||||
# Control which telemetry providers are active
|
||||
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
|
||||
# Set to false to disable Telemetry completely
|
||||
|
||||
# ============================================================================
|
||||
# OPENTELEMETRY (Optional - for advanced telemetry)
|
||||
# ============================================================================
|
||||
|
||||
@@ -97,7 +97,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
|
||||
@@ -138,7 +138,6 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
|
||||
@@ -38,6 +38,7 @@ coverage-unit
|
||||
src/generated/
|
||||
src/shared/proto/
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
@@ -1,10 +1,82 @@
|
||||
# Changelog
|
||||
|
||||
## [3.56.0]
|
||||
|
||||
### Added
|
||||
|
||||
- __CLI authentication:__ Added Vercel AI Gateway and Cline API key provider support for headless CI/automation workflows
|
||||
- __New model:__ Added Kimi-K2.5 model to Moonshot provider (262K context, image support, prompt caching)
|
||||
- __Prompt variant:__ Added Trinity Large prompt variant for improved tool-calling support
|
||||
- __OpenTelemetry:__ Added support for custom headers on metrics and logs endpoints
|
||||
- __Social links:__ Added community icons (X, Discord, GitHub, Reddit, LinkedIn) to the What's New modal
|
||||
|
||||
### Fixed
|
||||
|
||||
- __LiteLLM:__ Fixed thinking configuration not appearing for reasoning-capable models
|
||||
- __OpenTelemetry:__ Fixed endpoint path handling (no longer incorrectly appends `/v1/logs` or `/v1/metrics`) and ensured logs are sent regardless of VSCode telemetry settings
|
||||
- __CLI auth:__ Fixed `cline auth` displaying incorrect provider information after configuration
|
||||
|
||||
### Changed
|
||||
|
||||
- __Hooks:__ Hook scripts now run from the workspace repository root instead of filesystem root
|
||||
- __Default settings:__ Enabled multi-root workspaces, parallel tool calling, and skills by default; disabled strict plan mode by default
|
||||
- __Settings UI:__ Refreshed feature settings section with collapsible design
|
||||
|
||||
## [3.55.0]
|
||||
|
||||
- Add new model: Arcee Trinity Large Preview
|
||||
- Add new model: Moonshot Kimi K2.5
|
||||
- Add MCP prompts support - prompts from connected MCP servers now appear in slash command autocomplete as `/mcp:<server>:<prompt>`
|
||||
|
||||
## [3.54.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Native tool calls support for Ollama provider
|
||||
- Sonnet 4.5 is now the default Amazon Bedrock model id
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent infinite retry loops when replace_in_file fails repeatedly. The system now detects repeated failures and provides better guidance to break out of retry cycles.
|
||||
- Skip diff error UI handling during streaming to prevent flickering. Error handling is deferred until streaming completes.
|
||||
- Strip notebook cell outputs when extracting text content from Jupyter notebooks, significantly reducing context size sent to the LLM.
|
||||
- Throttle diff view updates during streaming to reduce UI flickering and improve performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed Mistral's Devstral-2512 free from the free models list
|
||||
- Removed deprecated zai-glm-4.6 model from Cerebras provider
|
||||
|
||||
## [3.53.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug in responses API
|
||||
|
||||
## [3.53.0]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed grok model from free tier
|
||||
|
||||
## [3.52.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
|
||||
- Grok models are now moving out of free tier and into paid plans.
|
||||
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bugs in DiffViewProvider for file editing
|
||||
- Ollama's recommended models to use correct identifiers
|
||||
|
||||
## [3.51.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
|
||||
## [3.50.0]
|
||||
|
||||
@@ -1737,4 +1809,4 @@ Add Opus 4.1 through Claude Code
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.317 4.15557C18.7873 3.45369 17.147 2.93658 15.4319 2.6404C15.4007 2.63469 15.3695 2.64897 15.3534 2.67754C15.1424 3.05276 14.9087 3.54225 14.7451 3.927C12.9004 3.65083 11.0652 3.65083 9.25832 3.927C9.09465 3.5337 8.85248 3.05276 8.64057 2.67754C8.62449 2.64992 8.59328 2.63564 8.56205 2.6404C6.84791 2.93563 5.20756 3.45275 3.67693 4.15557C3.66368 4.16129 3.65233 4.17082 3.64479 4.18319C0.533392 8.83155 -0.31895 13.3657 0.0991801 17.8436C0.101072 17.8655 0.11337 17.8864 0.130398 17.8997C2.18321 19.4073 4.17171 20.3225 6.12328 20.9291C6.15451 20.9386 6.18761 20.9272 6.20748 20.9015C6.66913 20.2711 7.08064 19.6063 7.43348 18.9073C7.4543 18.8664 7.43442 18.8178 7.39186 18.8016C6.73913 18.554 6.1176 18.2521 5.51973 17.9093C5.47244 17.8816 5.46865 17.814 5.51216 17.7816C5.63797 17.6873 5.76382 17.5893 5.88396 17.4902C5.90569 17.4721 5.93598 17.4683 5.96153 17.4797C9.88928 19.273 14.1415 19.273 18.023 17.4797C18.0485 17.4674 18.0788 17.4712 18.1015 17.4893C18.2216 17.5883 18.3475 17.6873 18.4742 17.7816C18.5177 17.814 18.5149 17.8816 18.4676 17.9093C17.8697 18.2588 17.2482 18.554 16.5945 18.8006C16.552 18.8168 16.533 18.8664 16.5538 18.9073C16.9143 19.6054 17.3258 20.2701 17.7789 20.9005C17.7978 20.9272 17.8319 20.9386 17.8631 20.9291C19.8241 20.3225 21.8126 19.4073 23.8654 17.8997C23.8834 17.8864 23.8948 17.8664 23.8967 17.8445C24.3971 12.6676 23.0585 8.17064 20.3482 4.18414C20.3416 4.17082 20.3303 4.16129 20.317 4.15557ZM8.02002 15.117C6.8375 15.117 5.86313 14.0313 5.86313 12.6981C5.86313 11.3648 6.8186 10.2791 8.02002 10.2791C9.23087 10.2791 10.1958 11.3743 10.1769 12.6981C10.1769 14.0313 9.22141 15.117 8.02002 15.117ZM15.9947 15.117C14.8123 15.117 13.8379 14.0313 13.8379 12.6981C13.8379 11.3648 14.7933 10.2791 15.9947 10.2791C17.2056 10.2791 18.1705 11.3743 18.1516 12.6981C18.1516 14.0313 17.2056 15.117 15.9947 15.117Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="black" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12C2 16.418 4.865 20.166 8.84 21.49C9.34 21.58 9.52 21.27 9.52 21C9.52 20.77 9.51 20.14 9.51 19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26C14.5 19.6 14.49 20.68 14.49 21C14.49 21.27 14.67 21.59 15.17 21.49C19.14 20.16 22 16.42 22 12C22 6.477 17.523 2 12 2Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 902 B |
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2001_1428)">
|
||||
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5562 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2937 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516Z" fill="#FAFAFA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2001_1428">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 989 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.0512 4.07466C15.3113 5.17727 16.301 5.99866 17.4829 5.99866C18.8627 5.99866 19.9813 4.87965 19.9813 3.49933C19.9813 2.11902 18.8627 1 17.4829 1C16.2764 1 15.2703 1.85537 15.036 2.99314C13.0155 3.20991 11.4378 4.92417 11.4378 7.00167C11.4378 7.00636 11.4378 7.00988 11.4378 7.01456C9.24041 7.10713 7.23397 7.73284 5.641 8.72062C5.04949 8.26247 4.30688 7.98945 3.50102 7.98945C1.5672 7.98945 0 9.55725 0 11.4918C0 12.8955 0.824597 14.1048 2.01581 14.6637C2.13177 18.7297 6.56047 22 12.0082 22C17.4559 22 21.8905 18.7261 22.0006 14.6567C23.1824 14.0942 24 12.8885 24 11.493C24 9.55842 22.4328 7.99063 20.499 7.99063C19.6966 7.99063 18.9575 8.2613 18.3672 8.71594C16.7602 7.72113 14.7315 7.09541 12.5119 7.01222C12.5119 7.0087 12.5119 7.00636 12.5119 7.00285C12.5119 5.51473 13.6176 4.27971 15.0512 4.077V4.07466ZM5.50044 13.7146C5.559 12.4444 6.40234 11.4695 7.38272 11.4695C8.3631 11.4695 9.11274 12.4995 9.05417 13.7697C8.99561 15.0398 8.26354 15.5015 7.28199 15.5015C6.30044 15.5015 5.44187 14.9848 5.50044 13.7146ZM16.6348 11.4695C17.6164 11.4695 18.4597 12.4444 18.5171 13.7146C18.5757 14.9848 17.716 15.5015 16.7356 15.5015C15.7552 15.5015 15.022 15.041 14.9634 13.7697C14.9048 12.4995 15.6533 11.4695 16.6348 11.4695ZM15.4682 16.6533C15.6521 16.6721 15.7693 16.8631 15.6978 17.0341C15.0946 18.4766 13.6703 19.4901 12.0082 19.4901C10.3461 19.4901 8.92299 18.4766 8.31859 17.0341C8.24714 16.8631 8.36427 16.6721 8.54817 16.6533C9.62577 16.5444 10.7912 16.4846 12.0082 16.4846C13.2252 16.4846 14.3895 16.5444 15.4682 16.6533Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3263 1.90393H21.6998L14.3297 10.3274L23 21.7899H16.2112L10.894 14.838L4.80995 21.7899H1.43443L9.31743 12.78L1 1.90393H7.96111L12.7674 8.25826L18.3263 1.90393ZM17.1423 19.7707H19.0116L6.94539 3.81706H4.93946L17.1423 19.7707Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -147,6 +147,29 @@
|
||||
"src/dev/grit/vscode-api.grit"
|
||||
]
|
||||
},
|
||||
{
|
||||
// Do not use console logging directly, use the Logger service instead.
|
||||
"plugins": [
|
||||
"src/dev/grit/console-log.grit"
|
||||
],
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/webview-ui/**",
|
||||
"!**/evals/**",
|
||||
"!**/standalone/**",
|
||||
"!**/e2e/**",
|
||||
"!**/test/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.stories.ts",
|
||||
"!src/dev/**",
|
||||
"!**/*.mjs",
|
||||
"!**/*.js",
|
||||
"!**/scripts/**",
|
||||
"!**/*.tsx",
|
||||
"!**/testing-platform/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"includes": [
|
||||
"**",
|
||||
|
||||
@@ -231,7 +231,7 @@ func promptForInitialTask(ctx context.Context, modeFlag string, workspaces []str
|
||||
|
||||
prompt, err := output.PromptForInitialTask(
|
||||
"Start a new Cline task",
|
||||
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
|
||||
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
|
||||
modeFlag,
|
||||
slash.NewRegistry(ctx),
|
||||
)
|
||||
|
||||
@@ -298,6 +298,64 @@ cline task view
|
||||
\f[I]# Start interactive chat with this task\f[R]
|
||||
cline task chat
|
||||
.EE
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
\f[B]CLINE_COMMAND_PERMISSIONS\f[R]
|
||||
JSON configuration for restricting which shell commands Cline can
|
||||
execute.
|
||||
When set, commands are validated against allow/deny patterns before
|
||||
execution.
|
||||
When not set, all commands are allowed (backward compatibility).
|
||||
.RS
|
||||
.PP
|
||||
Format:
|
||||
\f[CR]{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}\f[R]
|
||||
.PP
|
||||
\f[B]Fields:\f[R]
|
||||
.IP \(bu 2
|
||||
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
|
||||
If specified, only matching commands are permitted.
|
||||
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
|
||||
single character.
|
||||
.IP \(bu 2
|
||||
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
|
||||
Deny rules take precedence over allow rules.
|
||||
.IP \(bu 2
|
||||
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
|
||||
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
|
||||
Defaults to false.
|
||||
.PP
|
||||
\f[B]Rule evaluation:\f[R]
|
||||
.IP "1." 3
|
||||
Check for dangerous characters (backticks outside single quotes,
|
||||
unquoted newlines)
|
||||
.IP "2." 3
|
||||
Parse command into segments split by operators (\f[CR]&&\f[R],
|
||||
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
|
||||
.IP "3." 3
|
||||
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
|
||||
is denied
|
||||
.IP "4." 3
|
||||
Each segment is validated against deny rules first, then allow rules
|
||||
.IP "5." 3
|
||||
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
|
||||
recursively validated
|
||||
.IP "6." 3
|
||||
All segments must pass for the command to be allowed
|
||||
.PP
|
||||
\f[B]Examples:\f[R]
|
||||
.IP
|
||||
.EX
|
||||
\f[I]# Allow only npm and git commands\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *"]}\(aq
|
||||
|
||||
\f[I]# Allow development commands but deny dangerous ones\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *", "node *"], "deny": ["rm \-rf *", "sudo *"]}\(aq
|
||||
|
||||
\f[I]# Allow file operations with redirects\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["cat *", "echo *"], "allowRedirects": true}\(aq
|
||||
.EE
|
||||
.RE
|
||||
.SH ARCHITECTURE
|
||||
Cline operates on a three\-layer architecture:
|
||||
.TP
|
||||
|
||||
@@ -323,6 +323,42 @@ cline task view
|
||||
cline task chat
|
||||
```
|
||||
|
||||
# ENVIRONMENT
|
||||
|
||||
**CLINE_COMMAND_PERMISSIONS**
|
||||
|
||||
: JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patterns before execution. When not set, all commands are allowed.
|
||||
|
||||
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
|
||||
|
||||
**Fields:**
|
||||
|
||||
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
|
||||
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
|
||||
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
|
||||
|
||||
**Rule evaluation:**
|
||||
|
||||
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
|
||||
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
|
||||
3. If redirects detected and `allowRedirects` is not true, command is denied
|
||||
4. Each segment is validated against deny rules first, then allow rules
|
||||
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
|
||||
6. All segments must pass for the command to be allowed
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Allow only npm and git commands.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
|
||||
|
||||
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
|
||||
|
||||
# Allow file operations with redirects
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
|
||||
```
|
||||
|
||||
# ARCHITECTURE
|
||||
|
||||
Cline operates on a three-layer architecture:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
|
||||
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
@@ -45,8 +46,15 @@ const (
|
||||
// RunAuthFlow is the entry point for the entire auth flow with instance management
|
||||
// It spawns a fresh instance for auth operations and cleans it up when done
|
||||
func RunAuthFlow(ctx context.Context, args []string) error {
|
||||
// Spawn a fresh instance for auth operations
|
||||
instanceInfo, err := global.Instances.StartNewInstance(ctx)
|
||||
// Get current working directory to use as workspace
|
||||
// This ensures auth operations use the same state database as the main cline command
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current directory: %w", err)
|
||||
}
|
||||
|
||||
// Spawn a fresh instance for auth operations with the current workspace
|
||||
instanceInfo, err := global.Instances.StartNewInstance(ctx, cwd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start auth instance: %w", err)
|
||||
}
|
||||
@@ -187,9 +195,12 @@ func ShowAuthMenuWithStatus(isClineAuthenticated bool, hasOrganizations bool, cu
|
||||
var title string
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
|
||||
// Always show Cline authentication status
|
||||
// Show authentication status - distinguish between OAuth and API key auth
|
||||
if isClineAuthenticated {
|
||||
title = fmt.Sprintf("Cline Account: %s Authenticated\n", renderer.Green("✓"))
|
||||
} else if currentProvider == "Cline (Official)" {
|
||||
// Using Cline provider with API key (not OAuth)
|
||||
title = fmt.Sprintf("Cline API Key: %s Configured\n", renderer.Green("✓"))
|
||||
} else {
|
||||
title = fmt.Sprintf("Cline Account: %s Not authenticated\n", renderer.Red("✗"))
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
|
||||
// Provider not found - provide helpful error message
|
||||
supportedProviders := []string{
|
||||
"openai-native", "openai", "anthropic", "gemini",
|
||||
"openrouter", "xai", "cerebras", "ollama",
|
||||
"openrouter", "xai", "cerebras", "ollama", "vercel-ai-gateway", "cline",
|
||||
}
|
||||
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
|
||||
"invalid provider '%s'. Supported providers: %s",
|
||||
@@ -168,15 +168,17 @@ func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
|
||||
|
||||
// Validate against supported quick setup providers
|
||||
supportedProviders := map[cline.ApiProvider]bool{
|
||||
cline.ApiProvider_OPENAI_NATIVE: true,
|
||||
cline.ApiProvider_OPENAI: true,
|
||||
cline.ApiProvider_ANTHROPIC: true,
|
||||
cline.ApiProvider_GEMINI: true,
|
||||
cline.ApiProvider_OPENROUTER: true,
|
||||
cline.ApiProvider_XAI: true,
|
||||
cline.ApiProvider_CEREBRAS: true,
|
||||
cline.ApiProvider_OLLAMA: true,
|
||||
cline.ApiProvider_NOUSRESEARCH: true,
|
||||
cline.ApiProvider_OPENAI_NATIVE: true,
|
||||
cline.ApiProvider_OPENAI: true,
|
||||
cline.ApiProvider_ANTHROPIC: true,
|
||||
cline.ApiProvider_GEMINI: true,
|
||||
cline.ApiProvider_OPENROUTER: true,
|
||||
cline.ApiProvider_XAI: true,
|
||||
cline.ApiProvider_CEREBRAS: true,
|
||||
cline.ApiProvider_OLLAMA: true,
|
||||
cline.ApiProvider_NOUSRESEARCH: true,
|
||||
cline.ApiProvider_VERCEL_AI_GATEWAY: true,
|
||||
cline.ApiProvider_CLINE: true,
|
||||
}
|
||||
|
||||
if !supportedProviders[provider] {
|
||||
|
||||
@@ -14,9 +14,9 @@ type BYOProviderOption struct {
|
||||
}
|
||||
|
||||
// GetBYOProviderList returns the list of supported BYO providers for CLI configuration.
|
||||
// This list excludes Cline provider which is handled separately.
|
||||
func GetBYOProviderList() []BYOProviderOption {
|
||||
return []BYOProviderOption{
|
||||
{Name: "Cline (API Key)", Provider: cline.ApiProvider_CLINE},
|
||||
{Name: "Anthropic", Provider: cline.ApiProvider_ANTHROPIC},
|
||||
{Name: "OpenAI Compatible", Provider: cline.ApiProvider_OPENAI},
|
||||
{Name: "OpenAI (Official)", Provider: cline.ApiProvider_OPENAI_NATIVE},
|
||||
@@ -28,6 +28,7 @@ func GetBYOProviderList() []BYOProviderOption {
|
||||
{Name: "Cerebras", Provider: cline.ApiProvider_CEREBRAS},
|
||||
{Name: "NousResearch", Provider: cline.ApiProvider_NOUSRESEARCH},
|
||||
{Name: "Oracle Code Assist", Provider: cline.ApiProvider_OCA},
|
||||
{Name: "Vercel AI Gateway", Provider: cline.ApiProvider_VERCEL_AI_GATEWAY},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +106,10 @@ func GetBYOProviderPlaceholder(provider cline.ApiProvider) string {
|
||||
return "e.g., Hermes-4-405B"
|
||||
case cline.ApiProvider_OCA:
|
||||
return "e.g., oca/llama4"
|
||||
case cline.ApiProvider_VERCEL_AI_GATEWAY:
|
||||
return "e.g., anthropic/claude-sonnet-4.5"
|
||||
case cline.ApiProvider_CLINE:
|
||||
return "e.g., anthropic/claude-sonnet-4.5"
|
||||
default:
|
||||
return "Enter model ID"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/task"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"github.com/cline/grpc-go/client"
|
||||
)
|
||||
|
||||
// ProviderDisplay represents a configured provider for display purposes
|
||||
@@ -35,7 +36,14 @@ func GetProviderConfigurations(ctx context.Context) (*ProviderListResult, error)
|
||||
}
|
||||
|
||||
// Get latest state from Cline Core
|
||||
grpcClient, err := global.GetDefaultClient(ctx)
|
||||
// Use auth instance if available, otherwise fall back to default
|
||||
var grpcClient *client.ClineClient
|
||||
var err error
|
||||
if authAddr, ok := ctx.Value(authInstanceAddressKey).(string); ok && authAddr != "" {
|
||||
grpcClient, err = global.GetClientForAddress(ctx, authAddr)
|
||||
} else {
|
||||
grpcClient, err = global.GetDefaultClient(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get provider configs due to unable to get gRPC client: %w", err)
|
||||
}
|
||||
@@ -118,6 +126,7 @@ func (r *ProviderListResult) GetAllReadyProviders() []*ProviderDisplay {
|
||||
cline.ApiProvider_NOUSRESEARCH,
|
||||
cline.ApiProvider_OCA,
|
||||
cline.ApiProvider_HICAP,
|
||||
cline.ApiProvider_VERCEL_AI_GATEWAY,
|
||||
}
|
||||
|
||||
// Check each provider to see if it's ready to use
|
||||
@@ -248,6 +257,8 @@ func mapProviderStringToEnum(providerStr string) (cline.ApiProvider, bool) {
|
||||
return cline.ApiProvider_HICAP, true
|
||||
case "nousResearch":
|
||||
return cline.ApiProvider_NOUSRESEARCH, true
|
||||
case "vercel-ai-gateway":
|
||||
return cline.ApiProvider_VERCEL_AI_GATEWAY, true
|
||||
default:
|
||||
return cline.ApiProvider_ANTHROPIC, false // Return 0 value with false
|
||||
}
|
||||
@@ -283,6 +294,8 @@ func GetProviderIDForEnum(provider cline.ApiProvider) string {
|
||||
return "hicap"
|
||||
case cline.ApiProvider_NOUSRESEARCH:
|
||||
return "nousResearch"
|
||||
case cline.ApiProvider_VERCEL_AI_GATEWAY:
|
||||
return "vercel-ai-gateway"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -368,6 +381,8 @@ func GetProviderDisplayName(provider cline.ApiProvider) string {
|
||||
return "Hicap"
|
||||
case cline.ApiProvider_NOUSRESEARCH:
|
||||
return "NousResearch"
|
||||
case cline.ApiProvider_VERCEL_AI_GATEWAY:
|
||||
return "Vercel AI Gateway"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -464,11 +479,21 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
var configuredProviders []cline.ApiProvider
|
||||
|
||||
// Check for Cline provider (uses authentication instead of API key)
|
||||
if IsAuthenticated(ctx) {
|
||||
isOAuthAuth := IsAuthenticated(ctx)
|
||||
if isOAuthAuth {
|
||||
configuredProviders = append(configuredProviders, cline.ApiProvider_CLINE)
|
||||
verboseLog("[DEBUG] Cline provider is authenticated")
|
||||
}
|
||||
|
||||
// Also check for Cline API key (non-OAuth)
|
||||
if clineKey, ok := apiConfig["clineApiKey"].(string); ok && clineKey != "" {
|
||||
// Only add if not already added via OAuth
|
||||
if !isOAuthAuth {
|
||||
configuredProviders = append(configuredProviders, cline.ApiProvider_CLINE)
|
||||
verboseLog("[DEBUG] Cline provider configured with API key")
|
||||
}
|
||||
}
|
||||
|
||||
// Check OCA provider via global auth subscription (state presence)
|
||||
if state, _ := GetLatestOCAState(context.Background(), 2*time.Second); state != nil && state.User != nil {
|
||||
configuredProviders = append(configuredProviders, cline.ApiProvider_OCA)
|
||||
@@ -491,6 +516,7 @@ func DetectAllConfiguredProviders(ctx context.Context, manager *task.Manager) ([
|
||||
{cline.ApiProvider_CEREBRAS, []string{"cerebrasApiKey"}},
|
||||
{cline.ApiProvider_HICAP, []string{"hicapApiKey"}},
|
||||
{cline.ApiProvider_NOUSRESEARCH, []string{"nousResearchApiKey"}},
|
||||
{cline.ApiProvider_VERCEL_AI_GATEWAY, []string{"vercelAiGatewayApiKey"}},
|
||||
}
|
||||
|
||||
for _, providerCheck := range providersToCheck {
|
||||
|
||||
@@ -174,6 +174,17 @@ func GetProviderFields(provider cline.ApiProvider) (ProviderFields, error) {
|
||||
ActModeProviderSpecificModelIDField: "actModeNousResearchModelId",
|
||||
}, nil
|
||||
|
||||
case cline.ApiProvider_VERCEL_AI_GATEWAY:
|
||||
return ProviderFields{
|
||||
APIKeyField: "vercelAiGatewayApiKey",
|
||||
PlanModeModelIDField: "planModeApiModelId",
|
||||
ActModeModelIDField: "actModeApiModelId",
|
||||
PlanModeModelInfoField: "planModeVercelAiGatewayModelInfo",
|
||||
ActModeModelInfoField: "actModeVercelAiGatewayModelInfo",
|
||||
PlanModeProviderSpecificModelIDField: "planModeVercelAiGatewayModelId",
|
||||
ActModeProviderSpecificModelIDField: "actModeVercelAiGatewayModelId",
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return ProviderFields{}, fmt.Errorf("unsupported provider: %v", provider)
|
||||
}
|
||||
@@ -291,6 +302,8 @@ func setAPIKeyField(apiConfig *cline.ModelsApiConfiguration, fieldName string, v
|
||||
apiConfig.HicapApiKey = value
|
||||
case "nousResearchApiKey":
|
||||
apiConfig.NousResearchApiKey = value
|
||||
case "vercelAiGatewayApiKey":
|
||||
apiConfig.VercelAiGatewayApiKey = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +331,9 @@ func setProviderSpecificModelID(apiConfig *cline.ModelsApiConfiguration, fieldNa
|
||||
case "planModeNousResearchModelId":
|
||||
apiConfig.PlanModeNousResearchModelId = value
|
||||
apiConfig.ActModeNousResearchModelId = value
|
||||
case "planModeVercelAiGatewayModelId":
|
||||
apiConfig.PlanModeVercelAiGatewayModelId = value
|
||||
apiConfig.ActModeVercelAiGatewayModelId = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ func (m *Manager) ListSettings(ctx context.Context) error {
|
||||
"dictationSettings",
|
||||
"autoCondenseThreshold",
|
||||
"autoApprovalSettings",
|
||||
"hooksEnabled",
|
||||
}
|
||||
|
||||
// Render each field using the renderer
|
||||
|
||||
@@ -79,7 +79,7 @@ func RenderField(key string, value interface{}, censor bool) error {
|
||||
"planActSeparateModelsSetting", "enableCheckpointsSetting",
|
||||
"terminalReuseEnabled", "mcpResponsesCollapsed", "strictPlanModeEnabled",
|
||||
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
|
||||
"terminalOutputLineLimit", "autoCondenseThreshold", "hooksEnabled":
|
||||
"terminalOutputLineLimit", "autoCondenseThreshold":
|
||||
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
|
||||
return nil
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||
return ChangeInputTypeMsg{
|
||||
InputType: InputTypeFeedback,
|
||||
Title: "Your feedback",
|
||||
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
|
||||
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor\nctrl+c to exit",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error
|
||||
model := output.NewInputModelWithRegistry(
|
||||
output.InputTypeMessage,
|
||||
"Cline is ready for your message...",
|
||||
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
|
||||
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
|
||||
currentMode,
|
||||
ih.slashCommandRegistry,
|
||||
)
|
||||
|
||||
@@ -290,12 +290,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
return err
|
||||
}
|
||||
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
|
||||
case "hooks_enabled":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.HooksEnabled = boolPtr(val)
|
||||
case "azure_identity":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -104,14 +104,18 @@ func (s *EnvService) Shutdown(ctx context.Context, req *cline.EmptyRequest) (*cl
|
||||
return &cline.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *EnvService) isTelemetryEnabled() bool {
|
||||
// In CLI mode, check the CLINE_TELEMETRY_DISABLED environment variable
|
||||
return os.Getenv("CLINE_TELEMETRY_DISABLED") != "true"
|
||||
}
|
||||
|
||||
// GetTelemetrySettings returns the telemetry settings for CLI mode
|
||||
func (s *EnvService) GetTelemetrySettings(ctx context.Context, req *cline.EmptyRequest) (*host.GetTelemetrySettingsResponse, error) {
|
||||
if s.verbose {
|
||||
log.Printf("GetTelemetrySettings called")
|
||||
}
|
||||
|
||||
// In CLI mode, check the POSTHOG_TELEMETRY_ENABLED environment variable
|
||||
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
||||
telemetryEnabled := s.isTelemetryEnabled()
|
||||
|
||||
var setting host.Setting
|
||||
if telemetryEnabled {
|
||||
@@ -133,8 +137,7 @@ func (s *EnvService) SubscribeToTelemetrySettings(req *cline.EmptyRequest, strea
|
||||
log.Printf("SubscribeToTelemetrySettings called")
|
||||
}
|
||||
|
||||
// Send initial telemetry state
|
||||
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
||||
telemetryEnabled := s.isTelemetryEnabled()
|
||||
|
||||
var setting host.Setting
|
||||
if telemetryEnabled {
|
||||
|
||||
|
After Width: | Height: | Size: 8.9 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
@@ -117,7 +117,13 @@
|
||||
"features/auto-compact",
|
||||
"features/background-edit",
|
||||
"features/checkpoints",
|
||||
"features/cline-rules",
|
||||
{
|
||||
"group": "Cline Rules",
|
||||
"pages": [
|
||||
"features/cline-rules/overview",
|
||||
"features/cline-rules/conditional-rules"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Commands & Shortcuts",
|
||||
"pages": [
|
||||
@@ -148,6 +154,7 @@
|
||||
"features/hooks/samples"
|
||||
]
|
||||
},
|
||||
"features/jupyter-notebooks",
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
"features/skills",
|
||||
@@ -198,6 +205,7 @@
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
"provider-config/openai",
|
||||
"provider-config/openai-codex",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/cerebras",
|
||||
"provider-config/deepseek",
|
||||
@@ -422,6 +430,14 @@
|
||||
{
|
||||
"source": "/enterprise-solutions/team-management/roles-and-permissions",
|
||||
"destination": "/enterprise-solutions/team-management/managing-members"
|
||||
},
|
||||
{
|
||||
"source": "/features/cline-rules",
|
||||
"destination": "/features/cline-rules/overview"
|
||||
},
|
||||
{
|
||||
"source": "/features/conditional-rules",
|
||||
"destination": "/features/cline-rules/conditional-rules"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
Cline Rules allow you to provide Cline with system-level guidance. Think of them as a persistent way to include context and preferences for your projects or globally for every conversation.
|
||||
|
||||
## Creating a Rule
|
||||
|
||||
You can create a rule by clicking the `+` button in the Rules tab. This will open a new file in your IDE which you can use to write your rule.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
```markdown Example Cline Rule Structure [expandable]
|
||||
# Project Guidelines
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Update relevant documentation in /docs when modifying features
|
||||
- Keep README.md in sync with new capabilities
|
||||
- Maintain changelog entries in CHANGELOG.md
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Create ADRs in /docs/adr for:
|
||||
|
||||
- Major dependency changes
|
||||
- Architectural pattern changes
|
||||
- New integration patterns
|
||||
- Database schema changes
|
||||
Follow template in /docs/adr/template.md
|
||||
|
||||
## Code Style & Patterns
|
||||
|
||||
- Generate API clients using OpenAPI Generator
|
||||
- Use TypeScript axios template
|
||||
- Place generated code in /src/generated
|
||||
- Prefer composition over inheritance
|
||||
- Use repository pattern for data access
|
||||
- Follow error handling pattern in /src/utils/errors.ts
|
||||
|
||||
## Testing Standards
|
||||
|
||||
- Unit tests required for business logic
|
||||
- Integration tests for API endpoints
|
||||
- E2E tests for critical user flows
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
|
||||
2. **Team Consistency**: Ensures consistent behavior across all team members
|
||||
3. **Project-Specific**: Rules and standards tailored to each project's needs
|
||||
4. **Institutional Knowledge**: Maintains project standards and practices in code
|
||||
|
||||
Place the `.clinerules` file in your project's root directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules
|
||||
├── src/
|
||||
├── docs/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
|
||||
|
||||
### AGENTS.md Standard Support
|
||||
|
||||
Cline also supports the [AGENTS.md](https://agents.md/) standard as a fallback
|
||||
(in addition to Cline Rules) by automatically detecting `AGENTS.md` files in
|
||||
your workspace root. This allows you to use the same rules file across different AI
|
||||
coding tools.
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── AGENTS.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Tips for Writing Effective Cline Rules
|
||||
|
||||
- Be Clear and Concise: Use simple language and avoid ambiguity.
|
||||
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
|
||||
- Test and Iterate: Experiment to find what works best for your workflow.
|
||||
|
||||
### .clinerules/ Folder System
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Folder containing active rules
|
||||
│ ├── 01-coding.md # Core coding standards
|
||||
│ ├── 02-documentation.md # Documentation requirements
|
||||
│ └── current-sprint.md # Rules specific to current work
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline automatically processes **all Markdown files** inside the `.clinerules/` directory, combining them into a unified set of rules. The numeric prefixes (optional) help organize files in a logical sequence.
|
||||
|
||||
#### Using a Rules Bank
|
||||
|
||||
For projects with multiple contexts or teams, maintain a rules bank directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules - automatically applied
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Repository of available but inactive rules
|
||||
│ ├── clients/ # Client-specific rule sets
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ ├── frameworks/ # Framework-specific rules
|
||||
│ │ ├── react.md
|
||||
│ │ └── vue.md
|
||||
│ └── project-types/ # Project type standards
|
||||
│ ├── api-service.md
|
||||
│ └── frontend-app.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Benefits of the Folder Approach
|
||||
|
||||
1. **Contextual Activation**: Copy only relevant rules from the bank to the active folder
|
||||
2. **Easier Maintenance**: Update individual rule files without affecting others
|
||||
3. **Team Flexibility**: Different team members can activate rules specific to their current task
|
||||
4. **Reduced Noise**: Keep the active ruleset focused and relevant
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
Switch between client projects:
|
||||
|
||||
```bash
|
||||
# Switch to Client B project
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
Adapt to different tech stacks:
|
||||
|
||||
```bash
|
||||
# Frontend React project
|
||||
cp clinerules-bank/frameworks/react.md .clinerules/
|
||||
```
|
||||
|
||||
#### Implementation Tips
|
||||
|
||||
- Keep individual rule files focused on specific concerns
|
||||
- Use descriptive filenames that clearly indicate the rule's purpose
|
||||
- Consider git-ignoring the active `.clinerules/` folder while tracking the `clinerules-bank/`
|
||||
- Create team scripts to quickly activate common rule combinations
|
||||
|
||||
The folder system transforms your Cline rules from a static document into a dynamic knowledge system that adapts to your team's changing contexts and requirements.
|
||||
|
||||
### Managing Rules with the Toggleable Popover
|
||||
|
||||
To make managing both single `.clinerules` files and the folder system even easier, Cline v3.13 introduces a dedicated popover UI directly accessible from the chat interface.
|
||||
|
||||
Located conveniently under the chat input field, this popover allows you to:
|
||||
|
||||
- **Instantly See Active Rules:** View which global rules (from your user settings) and workspace rules (`.clinerules` file or folder contents) are currently active.
|
||||
- **Quickly Toggle Rules:** Enable or disable specific rule files within your workspace `.clinerules/` folder with a single click. This is perfect for activating context-specific rules (like `react-rules.md` or `memory-bank.md`) only when needed.
|
||||
- **Easily Add/Manage Rules:** Quickly create a workspace `.clinerules` file or folder if one doesn't exist, or add new rule files to an existing folder.
|
||||
|
||||
This UI significantly simplifies switching contexts and managing different sets of instructions without needing to manually edit files or configurations during a conversation.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Cline Logo" />
|
||||
</Frame>
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
title: "Conditional Rules"
|
||||
sidebarTitle: "Conditional Rules"
|
||||
description: "Activate rules automatically based on which files you're working with"
|
||||
---
|
||||
|
||||
Conditional rules let you scope rules to specific parts of your codebase. Rules activate only when you're working with matching files, keeping your context focused and relevant.
|
||||
|
||||
For an introduction to Cline Rules, see the [Overview](/features/cline-rules/overview).
|
||||
|
||||
- **Without conditionals**: every rule loads for every request.
|
||||
- **With conditionals**, rules activate only when your current files match their defined scope.
|
||||
|
||||
For example, React component rules should appear when you're working with React components, not when you're editing Python or documentation.
|
||||
|
||||
## How It Works
|
||||
|
||||
Conditional rules use YAML frontmatter at the top of your rule files. When Cline processes a request, it gathers context from your current work (open files, visible tabs, mentioned paths, edited files), evaluates each rule's conditions, and activates matching rules.
|
||||
|
||||
<Note>
|
||||
When a conditional rule activates, you'll see a notification: **"Conditional rules applied: workspace:frontend-rules.md"**
|
||||
</Note>
|
||||
|
||||
## Writing Conditional Rules
|
||||
|
||||
Add YAML frontmatter to the top of any rule file in your `.clinerules/` directory:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Component Guidelines
|
||||
|
||||
When creating or modifying React components:
|
||||
- Use functional components with React hooks
|
||||
- Extract reusable logic into custom React hooks
|
||||
- Keep components focused on a single responsibility
|
||||
```
|
||||
|
||||
The `---` markers delimit the frontmatter. Everything after the closing `---` is your rule content.
|
||||
|
||||
### The `paths` Conditional
|
||||
|
||||
Currently, `paths` is the supported conditional. It takes an array of glob patterns:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/**" # All files under src/
|
||||
- "*.config.js" # Config files in root
|
||||
- "packages/*/src/" # Monorepo package sources
|
||||
---
|
||||
```
|
||||
|
||||
**Glob pattern syntax:**
|
||||
- `*` matches any characters except `/`
|
||||
- `**` matches any characters including `/` (recursive)
|
||||
- `?` matches a single character
|
||||
- `[abc]` matches any character in the brackets
|
||||
- `{a,b}` matches either pattern
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Pattern | Matches |
|
||||
|---------|---------|
|
||||
| `src/**/*.ts` | All TypeScript files under `src/` |
|
||||
| `*.md` | Markdown files in root only |
|
||||
| `**/*.test.ts` | Test files anywhere in the project |
|
||||
| `packages/{web,api}/**` | Files in web or api packages |
|
||||
| `src/components/*.tsx` | TSX files directly in components (not nested) |
|
||||
|
||||
### Behavior Details
|
||||
|
||||
**Multiple patterns**: A rule activates if any pattern matches any file in your context.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "mobile/**"
|
||||
---
|
||||
# Activates when working in frontend OR mobile
|
||||
```
|
||||
|
||||
**No frontmatter**: Rules without frontmatter are always active.
|
||||
|
||||
**Empty paths array**: `paths: []` means the rule never activates. Use this to temporarily disable a rule.
|
||||
|
||||
**Invalid YAML**: If frontmatter can't be parsed, Cline fails open: the rule activates with raw content visible to help debugging.
|
||||
|
||||
## What Counts as "Current Context"
|
||||
|
||||
Cline evaluates rules based on:
|
||||
|
||||
1. **Your message**: File paths mentioned in your prompt (e.g., "update `src/App.tsx`")
|
||||
2. **Open tabs**: Files currently open in your editor
|
||||
3. **Visible files**: Files visible in your active editor panes
|
||||
4. **Edited files**: Files Cline has created, modified, or deleted during the task
|
||||
5. **Pending operations**: Files Cline is about to edit
|
||||
|
||||
Conditional rules can activate on your first message, when relevant files are open, or mid-task when Cline starts working with matching files.
|
||||
|
||||
<Tip>
|
||||
Be explicit about file paths in your prompts. "Update `src/services/user.ts`" reliably triggers path-based rules; "update the user service" may not.
|
||||
</Tip>
|
||||
|
||||
## Practical Examples
|
||||
|
||||
Copy these patterns and adapt them to your project structure.
|
||||
|
||||
### Frontend vs Backend Rules
|
||||
|
||||
Keep frontend and backend rules separate to avoid noise. Frontend rules only load when working with UI code, backend rules only load when working with API or service code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/frontend.md
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/pages/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# Frontend Guidelines
|
||||
|
||||
- Use Tailwind CSS for styling
|
||||
- Prefer server components where possible
|
||||
- Keep client components small and focused
|
||||
```
|
||||
|
||||
```yaml
|
||||
# .clinerules/backend.md
|
||||
---
|
||||
paths:
|
||||
- "src/api/**"
|
||||
- "src/services/**"
|
||||
- "src/db/**"
|
||||
---
|
||||
|
||||
# Backend Guidelines
|
||||
|
||||
- Use dependency injection for services
|
||||
- All database queries go through repositories
|
||||
- Return typed errors, not thrown exceptions
|
||||
```
|
||||
|
||||
### Test File Rules
|
||||
|
||||
Enforce testing standards automatically. This rule activates only when you're writing or modifying tests, so testing guidance appears exactly when you need it.
|
||||
|
||||
```yaml
|
||||
# .clinerules/testing.md
|
||||
---
|
||||
paths:
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
- "**/__tests__/**"
|
||||
---
|
||||
|
||||
# Testing Standards
|
||||
|
||||
- Use descriptive test names: "should [expected behavior] when [condition]"
|
||||
- One assertion per test when possible
|
||||
- Mock external dependencies, not internal modules
|
||||
- Use factories for test data, not fixtures
|
||||
```
|
||||
|
||||
### Documentation Rules
|
||||
|
||||
Apply documentation standards only when editing docs. Prevents style rules from cluttering your context when you're writing code.
|
||||
|
||||
```yaml
|
||||
# .clinerules/docs.md
|
||||
---
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "**/*.md"
|
||||
- "**/*.mdx"
|
||||
---
|
||||
|
||||
# Documentation Guidelines
|
||||
|
||||
- Use sentence case for headings
|
||||
- Include code examples for all features
|
||||
- Keep paragraphs short (3-4 sentences max)
|
||||
- Link to related documentation
|
||||
```
|
||||
|
||||
## Combining with Rule Toggles
|
||||
|
||||
Conditional rules work alongside the rule toggle UI. Toggle off a conditional rule to disable it entirely (it won't activate even if paths match). Toggle on to let it activate when conditions are met.
|
||||
|
||||
This provides two levels of control: manual toggles and automatic condition-based activation.
|
||||
|
||||
## Tips for Effective Conditional Rules
|
||||
|
||||
### Start Broad, Then Narrow
|
||||
|
||||
Begin with broader patterns and refine as you learn what works:
|
||||
|
||||
```yaml
|
||||
# Start here
|
||||
paths:
|
||||
- "src/**"
|
||||
|
||||
# Then narrow down
|
||||
paths:
|
||||
- "src/features/auth/**"
|
||||
```
|
||||
|
||||
### Use Descriptive Filenames
|
||||
|
||||
Name your rule files to indicate their scope:
|
||||
|
||||
```
|
||||
.clinerules/
|
||||
├── api-endpoints.md # Rules for API code
|
||||
├── database-models.md # Rules for DB layer
|
||||
├── react-components.md # Rules for React
|
||||
└── universal.md # No frontmatter = always active
|
||||
```
|
||||
|
||||
### Keep Universal Rules Separate
|
||||
|
||||
Put always-on rules (coding standards, project conventions) in files without frontmatter. Reserve conditional rules for context-specific guidance.
|
||||
|
||||
### Test Your Patterns
|
||||
|
||||
Not sure if a pattern matches? Create a simple test rule:
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "your/pattern/here/**"
|
||||
---
|
||||
|
||||
TEST: This rule should activate for your/pattern/here files.
|
||||
```
|
||||
|
||||
Then work with a file in that path and check if you see the activation notification.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Rule not activating:**
|
||||
- Check that file paths in your context match the glob pattern
|
||||
- Verify the rule is toggled on in the rules panel
|
||||
- Ensure YAML frontmatter has proper `---` delimiters
|
||||
|
||||
**Rule activating unexpectedly:**
|
||||
- Review glob patterns: `**` is recursive and may match more than intended
|
||||
- Check for open files that match the pattern
|
||||
- File paths mentioned in your message also count as context
|
||||
|
||||
**Frontmatter showing in output:**
|
||||
- YAML couldn't be parsed
|
||||
- Check for syntax errors (unquoted special characters, improper indentation)
|
||||
|
||||
## Related
|
||||
|
||||
- [Cline Rules Overview](/features/cline-rules/overview) - Complete rules system guide
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [@ Mentions](/features/at-mentions/overview) - Add files to context explicitly
|
||||
- [Understanding Context Management](/prompting/understanding-context-management) - How Cline manages context window
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: "Cline Rules"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Add persistent instructions and context to guide Cline's behavior"
|
||||
---
|
||||
|
||||
Cline Rules provide system-level guidance for your projects. Rules persist across conversations, ensuring consistent behavior without repeating instructions in every chat.
|
||||
|
||||
## How It Works
|
||||
|
||||
Rules are loaded when Cline starts a task. Here's what happens:
|
||||
|
||||
**Loading order**: Cline checks for rules in this sequence:
|
||||
1. `.clinerules/` folder (all `.md` files inside)
|
||||
2. Single `.clinerules` file
|
||||
3. `AGENTS.md` file
|
||||
|
||||
**Scope precedence**: Workspace rules override global rules when both define the same guidance.
|
||||
|
||||
**Multiple files**: When using a `.clinerules/` folder, all Markdown files are combined into one ruleset. Numeric prefixes (like `01-`, `02-`) control the order.
|
||||
|
||||
**Conditional activation**: Rules with YAML frontmatter activate only when you're working with matching files. See [Conditional Rules](/features/cline-rules/conditional-rules) for details.
|
||||
|
||||
## Supported Rule Files
|
||||
|
||||
Cline reads rules from multiple file formats in your workspace root, letting you share rules across different AI coding tools:
|
||||
|
||||
| File/Folder | Source | Notes |
|
||||
|-------------|--------|-------|
|
||||
| `.clinerules/` | Cline | Folder with `.md` files (recommended) |
|
||||
| `.cursor/rules/` | Cursor | Folder with `.mdc` files |
|
||||
| `.windsurf/rules` | Windsurf | Folder with multiple `md` files |
|
||||
| `AGENTS.md` | Universal | Follows [agents.md](https://agents.md/) standard, searched recursively |
|
||||
|
||||
Cline prioritizes `.clinerules` when present. Other formats load only if no `.clinerules` exists (except `AGENTS.md`, which always searches subdirectories). All rules appear in the Rules popover where you can toggle them.
|
||||
|
||||
## Creating Rules
|
||||
|
||||
Click the `+` button in the Rules tab to create a new rule. This opens a file in your editor where you write your guidance.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/cline-rules.png" alt="Create a Rule" />
|
||||
</Frame>
|
||||
|
||||
When you save the file, it's stored in:
|
||||
- **Workspace rules**: `.clinerules/` in your project root
|
||||
- **Global rules**: Platform-specific location (see table below)
|
||||
|
||||
You can also use the [`/newrule` slash command](/features/slash-commands/new-rule) to have Cline generate a rule based on your description.
|
||||
|
||||
### Global Rules Location
|
||||
|
||||
| Operating System | Default Location |
|
||||
|------------------|------------------|
|
||||
| **Windows** | `Documents\Cline\Rules` |
|
||||
| **macOS** | `~/Documents/Cline/Rules` |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` or `~/Cline/Rules` |
|
||||
|
||||
<Note>
|
||||
Linux/WSL users: Check both locations if you don't find global rules in `~/Documents/Cline/Rules`.
|
||||
</Note>
|
||||
|
||||
## Managing Rules
|
||||
|
||||
The Rules popover (below the chat input) shows active rules and lets you toggle them on or off.
|
||||
|
||||
<Frame>
|
||||
<img src="https://storage.googleapis.com/cline_public_images/docs/assets/image%20(1).png" alt="Rules Popover" />
|
||||
</Frame>
|
||||
|
||||
The popover displays:
|
||||
- **Global rules**: From your user-level Rules directory
|
||||
- **Workspace rules**: From `.clinerules/` in your project
|
||||
|
||||
Toggle any rule to enable or disable it. Disabled rules won't load, even if they match conditions.
|
||||
|
||||
## When to Use Rules
|
||||
|
||||
Rules work best for persistent project context:
|
||||
|
||||
- **Code standards**: Formatting preferences, naming conventions, project-specific patterns
|
||||
- **Documentation requirements**: Where to add docs, what format to follow
|
||||
- **Architecture decisions**: Design patterns, dependency rules, module boundaries
|
||||
- **Team conventions**: PR processes, branch naming, commit message format
|
||||
- **Technology constraints**: Required libraries, banned APIs, version requirements
|
||||
|
||||
Rules are less effective for:
|
||||
- One-time instructions (just say it in the chat)
|
||||
- Complex multi-step workflows (use [Workflows](/features/slash-commands/workflows/index) instead)
|
||||
- Dynamic decisions that depend on runtime context
|
||||
|
||||
## Example Rule
|
||||
|
||||
```markdown
|
||||
# Backend API Guidelines
|
||||
|
||||
## Route Handlers
|
||||
|
||||
- Use async/await, not callbacks
|
||||
- Validate request bodies with Zod schemas
|
||||
- Return typed errors from `src/errors.ts`
|
||||
- All routes require authentication unless in `publicRoutes` array
|
||||
|
||||
## Database Access
|
||||
|
||||
- All queries go through repository classes in `src/repositories/`
|
||||
- Use transactions for multi-table updates
|
||||
- Never expose raw database errors to clients
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests for business logic in `src/services/`
|
||||
- Integration tests for route handlers in `src/routes/`
|
||||
- Mock external APIs, not internal modules
|
||||
```
|
||||
|
||||
This rule provides clear, actionable guidance without explaining obvious concepts or using vague language.
|
||||
|
||||
## Using a Folder Structure
|
||||
|
||||
For projects with many rules, organize them in a `.clinerules/` folder:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/
|
||||
│ ├── 01-coding-standards.md
|
||||
│ ├── 02-documentation.md
|
||||
│ └── 03-testing.md
|
||||
├── src/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline loads all Markdown files in `.clinerules/` automatically. The numeric prefixes help you control ordering, but they're optional.
|
||||
|
||||
### Organizing a Rules Bank
|
||||
|
||||
Maintain a separate folder for rules you might need but don't always want active:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules/ # Active rules
|
||||
│ ├── 01-coding.md
|
||||
│ └── client-a.md
|
||||
│
|
||||
├── clinerules-bank/ # Available but inactive
|
||||
│ ├── clients/
|
||||
│ │ ├── client-a.md
|
||||
│ │ └── client-b.md
|
||||
│ └── frameworks/
|
||||
│ ├── react.md
|
||||
│ └── vue.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
Copy files from the bank to `.clinerules/` when you need them. This keeps your active context lean while maintaining a library of reusable guidance.
|
||||
|
||||
Switch contexts with simple file operations:
|
||||
|
||||
```bash
|
||||
# Switch to Client B
|
||||
rm .clinerules/client-a.md
|
||||
cp clinerules-bank/clients/client-b.md .clinerules/
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Consider git-ignoring `.clinerules/` while tracking `clinerules-bank/` so team members can activate the rules relevant to their current work.
|
||||
</Tip>
|
||||
|
||||
## Conditional Rules
|
||||
|
||||
Scope rules to specific file patterns using YAML frontmatter. This keeps React guidance out of Python code and backend rules away from frontend work.
|
||||
|
||||
```yaml
|
||||
---
|
||||
paths:
|
||||
- "src/components/**"
|
||||
- "src/hooks/**"
|
||||
---
|
||||
|
||||
# React Guidelines
|
||||
|
||||
Use functional components with hooks. Extract reusable logic into custom hooks.
|
||||
```
|
||||
|
||||
This rule activates only when working with files matching those patterns. Read the [Conditional Rules guide](/features/cline-rules/conditional-rules) for pattern syntax, behavior details, and more examples.
|
||||
|
||||
## Tips for Effective Rules
|
||||
|
||||
**Be specific**: "Use async/await for all database calls" beats "write good async code."
|
||||
|
||||
**Show patterns**: Include file paths and real examples. "Follow the error handling in `src/utils/errors.ts`" gives Cline a concrete reference.
|
||||
|
||||
**Focus on outcomes**: Describe what you want, not step-by-step instructions. Let Cline figure out how.
|
||||
|
||||
**Test and refine**: Start with core standards. Add rules when you find yourself repeating the same feedback.
|
||||
|
||||
**Use conditional rules**: Load guidance only when relevant. This keeps context efficient and reduces noise.
|
||||
|
||||
## Related
|
||||
|
||||
- [Conditional Rules](/features/cline-rules/conditional-rules) - Activate rules based on file patterns
|
||||
- [Skills](/features/skills) - Load instructions on demand with `/skill` command
|
||||
- [Workflows](/features/slash-commands/workflows/index) - Define explicit task automation
|
||||
- [New Rule Slash Command](/features/slash-commands/new-rule) - Generate rules with AI assistance
|
||||
- [Plan and Act Mode](/features/plan-and-act) - Use different rules for planning vs execution
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "Jupyter Notebooks"
|
||||
description: "AI-assisted editing of Jupyter notebooks with cell-level context awareness"
|
||||
---
|
||||
|
||||
# Jupyter Notebooks
|
||||
|
||||
Cline provides comprehensive support for Jupyter notebooks (`.ipynb` files), enabling AI-assisted editing with full cell-level context awareness. This feature was developed in collaboration with Amazon to bring AI coding assistance to data science workflows.
|
||||
|
||||
## Getting started
|
||||
|
||||
Jupyter notebook support is a built-in feature of Cline. To use it, you just need to have the Jupyter notebook extension enabled in VS Code. Once you open any `.ipynb` file, you'll see AI-assisted buttons in your notebook interface.
|
||||
|
||||
## How to use
|
||||
|
||||
### Generate Cell
|
||||
|
||||
Click the sparkle icon (✨) in the notebook toolbar to generate new cells with AI assistance.
|
||||
|
||||

|
||||
|
||||
**How it works:** The AI receives context from surrounding cells, so it understands the variables and imports already in scope. This means you can reference existing DataFrames, functions, and other objects without re-explaining them.
|
||||
|
||||
**Example prompt:** "Create a visualization showing the correlation matrix of numeric columns with a heatmap"
|
||||
|
||||
The cell is inserted with proper notebook JSON structure, preserving metadata and ready to execute.
|
||||
|
||||
### Explain Cell
|
||||
|
||||
Click the Explain button in any cell's title bar to get a detailed explanation of what the cell does.
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Revisiting old notebooks
|
||||
- Onboarding to a teammate's analysis
|
||||
- Understanding complex transformations
|
||||
|
||||
**How it works:** Cline extracts the full cell context, including outputs, so explanations can reference actual results like column names, row counts, and computed values.
|
||||
|
||||
### Improve Cell
|
||||
|
||||
Click the Improve button in any cell's title bar to enhance existing cells with AI suggestions.
|
||||
|
||||

|
||||
|
||||
Use this to:
|
||||
|
||||
- Optimize slow pandas operations
|
||||
- Add error handling
|
||||
- Refactor for clarity
|
||||
- Convert loops to vectorized operations
|
||||
|
||||
**How it works:** Cline suggests improvements while preserving the cell's position and metadata in the notebook structure. The AI explains what was changed and why.
|
||||
|
||||
## How cell context works
|
||||
|
||||
Unlike traditional file editing, Jupyter notebooks are JSON documents containing arrays of cells. Each cell has its own type, source content, metadata, execution count, and outputs.
|
||||
|
||||
When you use a Jupyter command, Cline extracts structured context that includes:
|
||||
|
||||
- **Cell type** (code, markdown, or raw)
|
||||
- **Source content** as an array of lines
|
||||
- **Cell metadata** and configuration
|
||||
- **Execution count** for code cells
|
||||
- **Outputs** including data, text, and error traces
|
||||
|
||||
This structured representation allows the AI to understand not just the code, but its context within the notebook and its actual output.
|
||||
|
||||
### JSON structure preservation
|
||||
|
||||
Cline is designed to work carefully with the cell JSON structure, aiming to:
|
||||
|
||||
- Keep cell boundaries intact
|
||||
- Preserve execution counts
|
||||
- Maintain cell metadata
|
||||
- Keep outputs associated with their source cells
|
||||
|
||||
The AI is specifically prompted to preserve notebook structure, though you should always review changes to ensure your notebook format remains correct.
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
You can bind any of these commands to keyboard shortcuts for faster access:
|
||||
|
||||
1. Open VS Code keyboard shortcuts (Cmd/Ctrl + K, Cmd/Ctrl + S)
|
||||
2. Search for `cline.jupyterGenerateCell`, `cline.jupyterExplainCell`, or `cline.jupyterImproveCell`
|
||||
3. Assign your preferred key combinations
|
||||
|
||||
## Tips for best results
|
||||
|
||||
**For Generate Cell:**
|
||||
- Be specific about what you want the cell to do
|
||||
- Reference existing variables by name (the AI can see them)
|
||||
- Mention preferred libraries if you have a preference (e.g., "use seaborn" or "use plotly")
|
||||
|
||||
**For Explain Cell:**
|
||||
- Works best on cells that have been executed (outputs provide additional context)
|
||||
- Good for complex chained operations like pandas groupby/merge sequences
|
||||
|
||||
**For Improve Cell:**
|
||||
- Mention what aspect you want to improve (performance, readability, error handling)
|
||||
- The AI will explain the changes it suggests
|
||||
|
||||
## Limitations
|
||||
|
||||
- Notebook support requires the Jupyter notebook extension to be enabled in VS Code
|
||||
- Cell context extraction depends on VS Code's notebook API
|
||||
- Very large notebooks may require more context than some models can handle efficiently
|
||||
|
||||
## Related
|
||||
|
||||
- [Cline Tools Guide](/exploring-clines-tools/cline-tools-guide) -- Overview of all Cline commands
|
||||
- [Model Selection Guide](/core-features/model-selection-guide) -- Choosing the right model for your workflow
|
||||
@@ -25,11 +25,28 @@ The easiest way to get started is with **Cline** as your provider:
|
||||
|
||||
## Alternative: Use Another Provider
|
||||
|
||||
If you prefer to use your own API keys, you can select from providers like:
|
||||
If you prefer to use your own accounts or API keys, you have several options:
|
||||
|
||||
### Sign in with OpenAI (Recommended)
|
||||
|
||||
The easiest way to use OpenAI models is with **OpenAI Codex**—no API keys needed:
|
||||
|
||||
1. Select **"OpenAI Codex"** from the API Provider dropdown
|
||||
2. Click **"Sign in with OpenAI"**
|
||||
3. Authorize Cline in your browser
|
||||
4. Choose your model
|
||||
|
||||
<Card title="OpenAI Codex Setup Guide" icon="key" href="/provider-config/openai-codex">
|
||||
See the full setup guide with screenshots and troubleshooting tips.
|
||||
</Card>
|
||||
|
||||
### Other Providers
|
||||
|
||||
You can also use API keys with these providers:
|
||||
|
||||
- **OpenRouter** - Great value, multiple models
|
||||
- **Anthropic** - Direct access to Claude models
|
||||
- **OpenAI** - Access to GPT models
|
||||
- **OpenAI** - Access to GPT models via API key
|
||||
- **Google Gemini** - Google's AI models
|
||||
- **Ollama** - Run models locally on your computer
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
|
||||
|
||||
Cline supports the following Cerebras models:
|
||||
|
||||
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
|
||||
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
|
||||
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
|
||||
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "OpenAI Codex"
|
||||
sidebarTitle: "OpenAI Codex"
|
||||
description: "Connect your OpenAI account to Cline via OAuth for seamless access to OpenAI models."
|
||||
---
|
||||
|
||||
OpenAI Codex lets you connect your OpenAI account directly to Cline using OAuth, meaning you don't need to manage API keys. Simply sign in with your OpenAI account through a one-click browser authentication, and you'll automatically have access to all the models available on your OpenAI plan. No additional configuration required.
|
||||
|
||||
<Tip>
|
||||
If you prefer to use API keys instead, see the [OpenAI (API Key)](/provider-config/openai) provider configuration.
|
||||
</Tip>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An [OpenAI account](https://platform.openai.com/signup)
|
||||
- An active OpenAI subscription or API access plan
|
||||
|
||||
<Note>
|
||||
The models available to you depend on your OpenAI account's subscription tier. See [OpenAI's pricing page](https://openai.com/pricing) for details on what's included in each plan.
|
||||
</Note>
|
||||
|
||||
## Connecting Your OpenAI Account
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Cline Settings">
|
||||
Click the settings gear icon in the top-right corner of the Cline extension.
|
||||
</Step>
|
||||
<Step title="Select OpenAI Codex Provider">
|
||||
Choose **"OpenAI Codex"** from the "API Provider" dropdown menu.
|
||||
</Step>
|
||||
<Step title="Sign In with OpenAI">
|
||||
Click the **"Sign in with OpenAI"** button. This will open a browser window for authentication.
|
||||
</Step>
|
||||
<Step title="Authorize Cline">
|
||||
In the browser window that opens:
|
||||
1. Sign in to your OpenAI account (if not already signed in)
|
||||
2. Review the permissions Cline is requesting
|
||||
3. Click **"Authorize"** to grant access
|
||||
</Step>
|
||||
<Step title="Select Your Model">
|
||||
Once authorized, you'll be returned to Cline. Choose your desired model from the **"Model"** dropdown.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
If a model doesn't appear in your dropdown, it may not be available on your OpenAI subscription tier.
|
||||
</Note>
|
||||
|
||||
For the most current list of available models and their capabilities, see the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
|
||||
|
||||
## Managing Your Connection
|
||||
|
||||
### Disconnecting Your Account
|
||||
|
||||
To disconnect your OpenAI account from Cline:
|
||||
|
||||
1. Open Cline Settings
|
||||
2. With OpenAI Codex selected as the provider, click **"Sign out"**
|
||||
3. Confirm the disconnection
|
||||
|
||||
This removes the OAuth connection. You can reconnect at any time by signing in again.
|
||||
|
||||
### Token Refresh
|
||||
|
||||
OAuth tokens are automatically refreshed by Cline. If you encounter authentication errors:
|
||||
|
||||
1. Try disconnecting and reconnecting your account
|
||||
2. Ensure your OpenAI account is in good standing
|
||||
3. Check that your subscription is active
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication failed" error
|
||||
|
||||
- Ensure you're signing in with the correct OpenAI account
|
||||
- Check that your OpenAI subscription is active
|
||||
- Try clearing your browser cache and signing in again
|
||||
|
||||
### Models not appearing
|
||||
|
||||
- The available models depend on your OpenAI subscription tier
|
||||
- Some models may require specific plan upgrades
|
||||
- Check [OpenAI's pricing page](https://openai.com/pricing) for model availability by plan
|
||||
|
||||
### OAuth window doesn't open
|
||||
|
||||
- Check if pop-ups are blocked in your browser
|
||||
- Try using a different browser
|
||||
- Ensure you have a stable internet connection
|
||||
|
||||
### Connection keeps expiring
|
||||
|
||||
- This is rare but can happen if your OpenAI account session expired
|
||||
- Disconnect and reconnect to refresh your authentication
|
||||
|
||||
## Good to Know
|
||||
|
||||
Cline only requests the permissions necessary to make API calls on your behalf. Your OpenAI credentials are never stored by Cline.
|
||||
|
||||
When using OpenAI Codex, Cline accesses models through your OpenAI/ChatGPT subscription. There is no separate per-token API billing or consumption of OpenAI API credits for this provider; usage and limits are governed by your ChatGPT (or OpenAI account) subscription plan, not by Cline.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [OpenAI (API Key)](/provider-config/openai): Alternative configuration using API keys
|
||||
- [Model Selection Guide](/core-features/model-selection-guide): Help choosing the right model
|
||||
@@ -144,10 +144,6 @@ if (process.env.ERROR_SERVICE_API_KEY) {
|
||||
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
|
||||
}
|
||||
|
||||
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
|
||||
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
|
||||
}
|
||||
|
||||
// OpenTelemetry configuration (injected at build time from GitHub secrets)
|
||||
// These provide production defaults that can be overridden at runtime via environment variables
|
||||
if (process.env.OTEL_TELEMETRY_ENABLED) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.51.0",
|
||||
"version": "3.56.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.51.0",
|
||||
"version": "3.56.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -1184,6 +1184,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2646,6 +2647,7 @@
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.9.15",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
"@types/node": ">=12.12.47"
|
||||
@@ -3229,6 +3231,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -3297,6 +3300,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -4912,8 +4916,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4926,8 +4929,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4940,8 +4942,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4954,8 +4955,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4968,8 +4968,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4982,8 +4981,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -4996,8 +4994,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5010,8 +5007,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5024,8 +5020,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5038,8 +5033,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5052,8 +5046,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5066,8 +5059,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5080,8 +5072,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5094,8 +5085,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5108,8 +5098,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5122,8 +5111,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5136,8 +5124,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5150,8 +5137,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5164,8 +5150,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5178,8 +5163,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5192,8 +5176,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5206,8 +5189,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -6770,8 +6752,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
@@ -6803,6 +6784,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -7519,6 +7501,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8320,6 +8303,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -8654,9 +8638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -9525,7 +9509,8 @@
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1342118",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
@@ -12476,6 +12461,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -12709,6 +12695,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
@@ -14860,9 +14847,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/open-graph-scraper/node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -15430,6 +15417,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15611,7 +15599,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -15632,7 +15619,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -16322,7 +16308,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -17800,7 +17785,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -17817,7 +17801,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -18178,6 +18161,7 @@
|
||||
"version": "5.5.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -18250,9 +18234,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz",
|
||||
"integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
@@ -18430,7 +18414,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18505,7 +18488,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -19193,6 +19175,7 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.51.0",
|
||||
"version": "3.56.2",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -75,6 +75,19 @@ message McpResourceTemplate {
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message McpPromptArgument {
|
||||
string name = 1;
|
||||
optional string description = 2;
|
||||
optional bool required = 3;
|
||||
}
|
||||
|
||||
message McpPrompt {
|
||||
string name = 1;
|
||||
optional string title = 2;
|
||||
optional string description = 3;
|
||||
repeated McpPromptArgument arguments = 4;
|
||||
}
|
||||
|
||||
enum McpServerStatus {
|
||||
// Protobuf enums (in proto3) must have a zero value defined, which serves as the default if the field isn't explicitly set.
|
||||
// To align with the required nature of the TypeScript type and avoid an unnecessary UNSPECIFIED state, we map one of the existing statuses to this zero value.
|
||||
@@ -95,6 +108,7 @@ message McpServer {
|
||||
optional int32 timeout = 9;
|
||||
optional bool oauth_required = 10;
|
||||
optional string oauth_auth_status = 11;
|
||||
repeated McpPrompt prompts = 12;
|
||||
}
|
||||
|
||||
message McpServers {
|
||||
|
||||
@@ -485,6 +485,7 @@ message LiteLLMModelInfo {
|
||||
repeated ModelTier tiers = 12;
|
||||
optional double temperature = 13;
|
||||
optional ApiFormat api_format = 14;
|
||||
optional bool supports_reasoning = 15;
|
||||
}
|
||||
|
||||
// Main ApiConfiguration message
|
||||
|
||||
@@ -100,6 +100,7 @@ message Secrets {
|
||||
optional string oca_api_key = 41;
|
||||
optional string oca_refresh_token = 42;
|
||||
optional string mcp_o_auth_secrets = 43;
|
||||
optional string cline_api_key = 44;
|
||||
}
|
||||
|
||||
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
|
||||
@@ -108,7 +109,6 @@ message Secrets {
|
||||
message Settings {
|
||||
optional string lite_llm_base_url = 1;
|
||||
optional bool lite_llm_use_prompt_cache = 2;
|
||||
map<string, string> open_ai_headers = 3;
|
||||
optional string anthropic_base_url = 4;
|
||||
optional string open_router_provider_sorting = 5;
|
||||
optional string aws_region = 6;
|
||||
@@ -257,7 +257,6 @@ message Settings {
|
||||
optional FocusChainSettings focus_chain_settings = 149;
|
||||
optional string custom_prompt = 150;
|
||||
optional double auto_condense_threshold = 151;
|
||||
optional bool hooks_enabled = 152;
|
||||
optional bool subagents_enabled = 153;
|
||||
optional bool enable_parallel_tool_calling = 154;
|
||||
optional bool background_edit_enabled = 155;
|
||||
@@ -278,6 +277,7 @@ message Settings {
|
||||
optional int32 open_telemetry_log_batch_timeout = 170;
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
optional bool worktrees_enabled = 172;
|
||||
map<string, string> open_ai_headers = 173;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -384,6 +384,8 @@ message UpdateTaskSettingsRequest {
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
|
||||
Metadata metadata = 1;
|
||||
optional ModelsApiConfiguration api_configuration = 2;
|
||||
optional string telemetry_setting = 3;
|
||||
@@ -408,7 +410,6 @@ message UpdateSettingsRequest {
|
||||
optional DictationSettings dictation_settings = 23;
|
||||
optional double auto_condense_threshold = 24;
|
||||
optional bool multi_root_enabled = 25;
|
||||
optional bool hooks_enabled = 26;
|
||||
optional string vscode_terminal_execution_mode = 27;
|
||||
optional int32 max_consecutive_mistakes = 28;
|
||||
optional bool subagents_enabled = 29;
|
||||
|
||||
@@ -70,6 +70,7 @@ enum ClineSay {
|
||||
HOOK_STATUS = 30;
|
||||
HOOK_OUTPUT_STREAM = 31;
|
||||
COMMAND_PERMISSION_DENIED = 32;
|
||||
CONDITIONAL_RULES_APPLIED = 33;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
|
||||
@@ -33,6 +33,9 @@ service EnvService {
|
||||
|
||||
// Initiates a graceful shutdown of the host bridge service.
|
||||
rpc shutdown(cline.EmptyRequest) returns (cline.Empty);
|
||||
|
||||
// Logs a debug message to the host environment's log/output console.
|
||||
rpc debugLog(cline.StringRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
message GetHostVersionResponse {
|
||||
|
||||
@@ -26,7 +26,7 @@ const TS_PROTO_PLUGIN = isWindows
|
||||
: require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
|
||||
const TS_PROTO_OPTIONS = [
|
||||
"env=node",
|
||||
"env=both",
|
||||
"esModuleInterop=true",
|
||||
"outputServices=generic-definitions", // output generic ServiceDefinitions
|
||||
"outputIndex=true", // output an index file for each package which exports all protos in the package.
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import sinon from "sinon"
|
||||
import { ClineConfigurationError, ClineEndpoint, ClineEnv, Environment } from "../config"
|
||||
|
||||
describe("ClineEndpoint configuration", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let tempDir: string
|
||||
let originalHomedir: typeof os.homedir
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
tempDir = path.join(os.tmpdir(), `config-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
|
||||
// Create .cline directory
|
||||
await fs.mkdir(path.join(tempDir, ".cline"), { recursive: true })
|
||||
|
||||
// Stub os.homedir to return our temp directory
|
||||
originalHomedir = os.homedir
|
||||
sandbox
|
||||
.stub(os, "homedir")
|
||||
.returns(tempDir)
|
||||
|
||||
// Reset the singleton state using internal method
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
;(ClineEndpoint as any)._initialized = false
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore()
|
||||
// Reset singleton state
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
;(ClineEndpoint as any)._initialized = false
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
describe("valid config parsing", () => {
|
||||
it("should parse valid endpoints.json with all required fields", async () => {
|
||||
const validConfig = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://app.enterprise.com")
|
||||
config.apiBaseUrl.should.equal("https://api.enterprise.com")
|
||||
config.mcpBaseUrl.should.equal("https://mcp.enterprise.com")
|
||||
config.environment.should.equal(Environment.selfHosted)
|
||||
})
|
||||
|
||||
it("should work without endpoints.json (standard mode)", async () => {
|
||||
// No endpoints.json file exists
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.environment.should.not.equal(Environment.selfHosted)
|
||||
// Should use production defaults
|
||||
config.appBaseUrl.should.equal("https://app.cline.bot")
|
||||
config.apiBaseUrl.should.equal("https://api.cline.bot")
|
||||
})
|
||||
|
||||
it("should accept URLs with ports", async () => {
|
||||
const validConfig = {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
apiBaseUrl: "http://localhost:7777",
|
||||
mcpBaseUrl: "http://localhost:8080/mcp",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("http://localhost:3000")
|
||||
config.apiBaseUrl.should.equal("http://localhost:7777")
|
||||
config.mcpBaseUrl.should.equal("http://localhost:8080/mcp")
|
||||
})
|
||||
|
||||
it("should accept URLs with paths", async () => {
|
||||
const validConfig = {
|
||||
appBaseUrl: "https://proxy.enterprise.com/cline/app",
|
||||
apiBaseUrl: "https://proxy.enterprise.com/cline/api",
|
||||
mcpBaseUrl: "https://proxy.enterprise.com/cline/mcp",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(validConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://proxy.enterprise.com/cline/app")
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid JSON handling", () => {
|
||||
it("should throw ClineConfigurationError for invalid JSON syntax", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{ invalid json }", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Invalid JSON")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for truncated JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '{"appBaseUrl": "https://test.com"', "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Invalid JSON")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for empty file", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for non-object JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), '"just a string"', "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must contain a JSON object")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for array JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "[]", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
// Arrays pass the object check but fail on required fields
|
||||
error.message.should.containEql("Missing required field")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for null JSON", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "null", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must contain a JSON object")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("missing required fields", () => {
|
||||
it("should throw ClineConfigurationError when appBaseUrl is missing", async () => {
|
||||
const config = {
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "appBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when apiBaseUrl is missing", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "apiBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when mcpBaseUrl is missing", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "mcpBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when all fields are missing", async () => {
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), "{}", "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("Missing required field")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is null", async () => {
|
||||
const config = {
|
||||
appBaseUrl: null,
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql('Missing required field "appBaseUrl"')
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is empty string", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("cannot be empty")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is whitespace only", async () => {
|
||||
const config = {
|
||||
appBaseUrl: " ",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("cannot be empty")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError when field is non-string", async () => {
|
||||
const config = {
|
||||
appBaseUrl: 12345,
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a string")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid URL detection", () => {
|
||||
it("should throw ClineConfigurationError for invalid URL format", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "not-a-valid-url",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a valid URL")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for URL without protocol", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a valid URL")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw ClineConfigurationError for malformed URL", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql("must be a valid URL")
|
||||
}
|
||||
})
|
||||
|
||||
it("should include the invalid URL value in error message", async () => {
|
||||
const invalidUrl = "definitely-not-a-url"
|
||||
const config = {
|
||||
appBaseUrl: invalidUrl,
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
try {
|
||||
await ClineEndpoint.initialize()
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.should.be.instanceof(ClineConfigurationError)
|
||||
error.message.should.containEql(invalidUrl)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("environment switching blocked in self-hosted mode", () => {
|
||||
it("should throw error when trying to change environment in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
// Verify we're in self-hosted mode
|
||||
ClineEndpoint.config.environment.should.equal(Environment.selfHosted)
|
||||
|
||||
// Try to change environment - should throw
|
||||
try {
|
||||
ClineEnv.setEnvironment("staging")
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.message.should.containEql("Cannot change environment in on-premise mode")
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw error for all environment values in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const environments = ["staging", "local", "production", "anything"]
|
||||
for (const env of environments) {
|
||||
try {
|
||||
ClineEnv.setEnvironment(env)
|
||||
throw new Error(`Should have thrown for environment: ${env}`)
|
||||
} catch (error: any) {
|
||||
error.message.should.containEql("Cannot change environment in on-premise mode")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should allow environment switching in standard mode", async () => {
|
||||
// No endpoints.json file - standard mode
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
// Verify we're NOT in self-hosted mode
|
||||
ClineEndpoint.config.environment.should.not.equal(Environment.selfHosted)
|
||||
|
||||
// Should be able to change environment
|
||||
ClineEnv.setEnvironment("staging")
|
||||
ClineEnv.getEnvironment().environment.should.equal("staging")
|
||||
|
||||
ClineEnv.setEnvironment("local")
|
||||
ClineEnv.getEnvironment().environment.should.equal("local")
|
||||
|
||||
ClineEnv.setEnvironment("production")
|
||||
ClineEnv.getEnvironment().environment.should.equal("production")
|
||||
})
|
||||
})
|
||||
|
||||
describe("self-hosted mode behavior", () => {
|
||||
it("should report selfHosted environment in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const envConfig = ClineEndpoint.config
|
||||
envConfig.environment.should.equal(Environment.selfHosted)
|
||||
})
|
||||
|
||||
it("should use custom endpoints from file", async () => {
|
||||
const customConfig = {
|
||||
appBaseUrl: "https://custom-app.internal",
|
||||
apiBaseUrl: "https://custom-api.internal",
|
||||
mcpBaseUrl: "https://custom-mcp.internal/v1",
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(customConfig), "utf8")
|
||||
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
const config = ClineEndpoint.config
|
||||
config.appBaseUrl.should.equal("https://custom-app.internal")
|
||||
config.apiBaseUrl.should.equal("https://custom-api.internal")
|
||||
config.mcpBaseUrl.should.equal("https://custom-mcp.internal/v1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("initialization behavior", () => {
|
||||
it("should only initialize once", async () => {
|
||||
await ClineEndpoint.initialize()
|
||||
ClineEndpoint.isInitialized().should.be.true()
|
||||
|
||||
// Second initialize should be a no-op
|
||||
await ClineEndpoint.initialize()
|
||||
ClineEndpoint.isInitialized().should.be.true()
|
||||
})
|
||||
|
||||
it("should throw error when accessing config before initialization", async () => {
|
||||
// Already reset in beforeEach, so accessing should throw
|
||||
try {
|
||||
const _ = ClineEndpoint.config
|
||||
throw new Error("Should have thrown")
|
||||
} catch (error: any) {
|
||||
error.message.should.containEql("not initialized")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("isSelfHosted() method", () => {
|
||||
it("should return true when not initialized (safety fallback)", async () => {
|
||||
// Reset singleton state - already done in beforeEach, not initialized
|
||||
ClineEndpoint.isInitialized().should.be.false()
|
||||
ClineEndpoint.isSelfHosted().should.be.true()
|
||||
})
|
||||
|
||||
it("should return true when in self-hosted mode", async () => {
|
||||
const config = {
|
||||
appBaseUrl: "https://app.enterprise.com",
|
||||
apiBaseUrl: "https://api.enterprise.com",
|
||||
mcpBaseUrl: "https://mcp.enterprise.com",
|
||||
}
|
||||
await fs.writeFile(path.join(tempDir, ".cline", "endpoints.json"), JSON.stringify(config), "utf8")
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
ClineEndpoint.isSelfHosted().should.be.true()
|
||||
})
|
||||
|
||||
it("should return false when in normal mode (no endpoints.json)", async () => {
|
||||
// No endpoints.json file exists
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
ClineEndpoint.isSelfHosted().should.be.false()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,18 +2,18 @@ import * as vscode from "vscode"
|
||||
import {
|
||||
cleanupMcpMarketplaceCatalogFromGlobalState,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateHooksEnabledToBoolean,
|
||||
migrateTaskHistoryToFile,
|
||||
migrateWelcomeViewCompleted,
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
import { BannerService } from "./services/banner/BannerService"
|
||||
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
|
||||
@@ -26,19 +26,30 @@ import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { syncWorker } from "./shared/services/worker/sync"
|
||||
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
import { arePathsEqual } from "./utils/path"
|
||||
|
||||
/**
|
||||
* Performs intialization for Cline that is common to all platforms.
|
||||
*
|
||||
* @param context
|
||||
* @returns The webview provider
|
||||
* @throws ClineConfigurationError if endpoints.json exists but is invalid
|
||||
*/
|
||||
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
|
||||
// Configure the shared Logging class to use HostProvider's output channels and debug logger
|
||||
Logger.subscribe((msg: string) => HostProvider.get().logToChannel(msg)) // File system logging
|
||||
Logger.subscribe((msg: string) => HostProvider.env.debugLog({ value: msg })) // Host debug logging
|
||||
|
||||
// Initialize ClineEndpoint configuration first (reads ~/.cline/endpoints.json if present)
|
||||
// This must be done before any other code that calls ClineEnv.config()
|
||||
// Throws ClineConfigurationError if config file exists but is invalid
|
||||
const { ClineEndpoint } = await import("./config")
|
||||
await ClineEndpoint.initialize()
|
||||
|
||||
try {
|
||||
await StateManager.initialize(context)
|
||||
} catch (error) {
|
||||
console.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to initialize Cline's application state. Please restart the extension.",
|
||||
@@ -51,8 +62,10 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
// Set the distinct ID for logging and telemetry
|
||||
await initializeDistinctId(context)
|
||||
|
||||
// Initialize PostHog client provider
|
||||
PostHogClientProvider.getInstance()
|
||||
// Initialize PostHog client provider (skip in self-hosted mode)
|
||||
if (!ClineEndpoint.isSelfHosted()) {
|
||||
PostHogClientProvider.getInstance()
|
||||
}
|
||||
|
||||
// Setup the external services
|
||||
await ErrorService.initialize()
|
||||
@@ -70,9 +83,6 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
// Ensure taskHistory.json exists and migrate legacy state (runs once)
|
||||
await migrateTaskHistoryToFile(context)
|
||||
|
||||
// Migrate hooksEnabled from ClineFeatureSetting to boolean (one-time cleanup)
|
||||
await migrateHooksEnabledToBoolean(context)
|
||||
|
||||
// Clean up MCP marketplace catalog from global state (moved to disk cache)
|
||||
await cleanupMcpMarketplaceCatalogFromGlobalState(context)
|
||||
|
||||
@@ -127,7 +137,7 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
Logger.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
export enum Environment {
|
||||
production = "production",
|
||||
staging = "staging",
|
||||
local = "local",
|
||||
}
|
||||
import * as fs from "fs/promises"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Environment, type EnvironmentConfig } from "./shared/config-types"
|
||||
import { Logger } from "./shared/services/Logger"
|
||||
|
||||
export interface EnvironmentConfig {
|
||||
environment: Environment
|
||||
export { Environment, type EnvironmentConfig }
|
||||
|
||||
/**
|
||||
* Schema for the endpoints.json configuration file used in on-premise deployments.
|
||||
* All fields are required and must be valid URLs.
|
||||
*/
|
||||
interface EndpointsFileSchema {
|
||||
appBaseUrl: string
|
||||
apiBaseUrl: string
|
||||
mcpBaseUrl: string
|
||||
}
|
||||
|
||||
class ClineEndpoint {
|
||||
public static instance = new ClineEndpoint()
|
||||
public static get config() {
|
||||
return ClineEndpoint.instance.config()
|
||||
/**
|
||||
* Error thrown when the Cline configuration file exists but is invalid.
|
||||
* This error prevents Cline from starting to avoid misconfiguration in enterprise environments.
|
||||
*/
|
||||
export class ClineConfigurationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = "ClineConfigurationError"
|
||||
}
|
||||
}
|
||||
|
||||
class ClineEndpoint {
|
||||
private static _instance: ClineEndpoint | null = null
|
||||
private static _initialized = false
|
||||
|
||||
// On-premise config loaded from file (null if not on-premise)
|
||||
private onPremiseConfig: EndpointsFileSchema | null = null
|
||||
private environment: Environment = Environment.production
|
||||
|
||||
private constructor() {
|
||||
@@ -24,15 +40,189 @@ class ClineEndpoint {
|
||||
const _env = process?.env?.CLINE_ENVIRONMENT_OVERRIDE || process?.env?.CLINE_ENVIRONMENT
|
||||
if (_env && Object.values(Environment).includes(_env as Environment)) {
|
||||
this.environment = _env as Environment
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the ClineEndpoint singleton.
|
||||
* Must be called before any other methods.
|
||||
* Reads the endpoints.json file if it exists and validates its schema.
|
||||
*
|
||||
* @throws ClineConfigurationError if the endpoints.json file exists but is invalid
|
||||
*/
|
||||
public static async initialize(): Promise<void> {
|
||||
if (ClineEndpoint._initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
ClineEndpoint._instance = new ClineEndpoint()
|
||||
|
||||
// Try to load on-premise config from file
|
||||
const endpointsConfig = await ClineEndpoint.loadEndpointsFile()
|
||||
if (endpointsConfig) {
|
||||
ClineEndpoint._instance.onPremiseConfig = endpointsConfig
|
||||
Logger.log("Cline running in self-hosted mode with custom endpoints")
|
||||
}
|
||||
|
||||
ClineEndpoint._initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the ClineEndpoint has been initialized.
|
||||
*/
|
||||
public static isInitialized(): boolean {
|
||||
return ClineEndpoint._initialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Cline is running in self-hosted/on-premise mode.
|
||||
* @returns true if in selfHosted mode, or true if not initialized (safety fallback to prevent accidental external calls)
|
||||
*/
|
||||
public static isSelfHosted(): boolean {
|
||||
// Safety fallback: if not initialized, treat as selfHosted
|
||||
// to prevent accidental external service calls before configuration is loaded
|
||||
if (!ClineEndpoint._initialized) {
|
||||
return true
|
||||
}
|
||||
return ClineEndpoint.config.environment === Environment.selfHosted
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the singleton instance.
|
||||
* @throws Error if not initialized
|
||||
*/
|
||||
public static get instance(): ClineEndpoint {
|
||||
if (!ClineEndpoint._initialized || !ClineEndpoint._instance) {
|
||||
throw new Error("ClineEndpoint not initialized. Call ClineEndpoint.initialize() first.")
|
||||
}
|
||||
return ClineEndpoint._instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Static getter for convenient access to the current configuration.
|
||||
* @throws Error if not initialized
|
||||
*/
|
||||
public static get config(): EnvironmentConfig {
|
||||
return ClineEndpoint.instance.config()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the endpoints.json configuration file.
|
||||
* Located at ~/.cline/endpoints.json
|
||||
*/
|
||||
private static getEndpointsFilePath(): string {
|
||||
return path.join(os.homedir(), ".cline", "endpoints.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and validates the endpoints.json file.
|
||||
* @returns The validated endpoints config, or null if the file doesn't exist
|
||||
* @throws ClineConfigurationError if the file exists but is invalid
|
||||
*/
|
||||
private static async loadEndpointsFile(): Promise<EndpointsFileSchema | null> {
|
||||
const filePath = ClineEndpoint.getEndpointsFilePath()
|
||||
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch {
|
||||
// File doesn't exist - not on-premise mode
|
||||
return null
|
||||
}
|
||||
|
||||
// File exists, must be valid or we fail
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, "utf8")
|
||||
let data: unknown
|
||||
|
||||
try {
|
||||
data = JSON.parse(fileContent)
|
||||
} catch (parseError) {
|
||||
throw new ClineConfigurationError(
|
||||
`Invalid JSON in endpoints configuration file (${filePath}): ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return ClineEndpoint.validateEndpointsSchema(data, filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof ClineConfigurationError) {
|
||||
throw error
|
||||
}
|
||||
throw new ClineConfigurationError(
|
||||
`Failed to read endpoints configuration file (${filePath}): ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the provided data matches the EndpointsFileSchema.
|
||||
* All fields must be present and be valid URLs.
|
||||
*
|
||||
* @param data The parsed JSON data to validate
|
||||
* @param filePath The path to the file (for error messages)
|
||||
* @returns The validated EndpointsFileSchema
|
||||
* @throws ClineConfigurationError if validation fails
|
||||
*/
|
||||
private static validateEndpointsSchema(data: unknown, filePath: string): EndpointsFileSchema {
|
||||
if (typeof data !== "object" || data === null) {
|
||||
throw new ClineConfigurationError(`Endpoints configuration file (${filePath}) must contain a JSON object`)
|
||||
}
|
||||
|
||||
const obj = data as Record<string, unknown>
|
||||
const requiredFields = ["appBaseUrl", "apiBaseUrl", "mcpBaseUrl"] as const
|
||||
const result: Partial<EndpointsFileSchema> = {}
|
||||
|
||||
for (const field of requiredFields) {
|
||||
const value = obj[field]
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
throw new ClineConfigurationError(
|
||||
`Missing required field "${field}" in endpoints configuration file (${filePath})`,
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
throw new ClineConfigurationError(
|
||||
`Field "${field}" in endpoints configuration file (${filePath}) must be a string`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!value.trim()) {
|
||||
throw new ClineConfigurationError(
|
||||
`Field "${field}" in endpoints configuration file (${filePath}) cannot be empty`,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(value)
|
||||
} catch {
|
||||
throw new ClineConfigurationError(
|
||||
`Field "${field}" in endpoints configuration file (${filePath}) must be a valid URL. Got: "${value}"`,
|
||||
)
|
||||
}
|
||||
|
||||
result[field] = value
|
||||
}
|
||||
|
||||
return result as EndpointsFileSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current environment configuration.
|
||||
*/
|
||||
public config(): EnvironmentConfig {
|
||||
return this.getEnvironment()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current environment.
|
||||
* @throws Error if in on-premise mode (environment switching is disabled)
|
||||
*/
|
||||
public setEnvironment(env: string) {
|
||||
if (this.onPremiseConfig) {
|
||||
throw new Error("Cannot change environment in on-premise mode. Endpoints are configured via ~/.cline/endpoints.json")
|
||||
}
|
||||
|
||||
switch (env.toLowerCase()) {
|
||||
case "staging":
|
||||
this.environment = Environment.staging
|
||||
@@ -44,10 +234,24 @@ class ClineEndpoint {
|
||||
this.environment = Environment.production
|
||||
break
|
||||
}
|
||||
console.info("Cline environment updated: ", this.environment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current environment configuration.
|
||||
* If running in on-premise mode, returns the custom endpoints.
|
||||
*/
|
||||
public getEnvironment(): EnvironmentConfig {
|
||||
// On-premise mode: use custom endpoints from file
|
||||
if (this.onPremiseConfig) {
|
||||
return {
|
||||
environment: Environment.selfHosted,
|
||||
appBaseUrl: this.onPremiseConfig.appBaseUrl,
|
||||
apiBaseUrl: this.onPremiseConfig.apiBaseUrl,
|
||||
mcpBaseUrl: this.onPremiseConfig.mcpBaseUrl,
|
||||
}
|
||||
}
|
||||
|
||||
// Standard mode: use built-in environment URLs
|
||||
switch (this.environment) {
|
||||
case Environment.staging:
|
||||
return {
|
||||
@@ -79,5 +283,14 @@ class ClineEndpoint {
|
||||
* Usage:
|
||||
* - ClineEnv.config() to get the current config.
|
||||
* - ClineEnv.setEnvironment(Environment.local) to change the environment.
|
||||
*
|
||||
* IMPORTANT: ClineEndpoint.initialize() must be called before using ClineEnv.
|
||||
*/
|
||||
export const ClineEnv = ClineEndpoint.instance
|
||||
export const ClineEnv = {
|
||||
config: () => ClineEndpoint.config,
|
||||
setEnvironment: (env: string) => ClineEndpoint.instance.setEnvironment(env),
|
||||
getEnvironment: () => ClineEndpoint.instance.getEnvironment(),
|
||||
}
|
||||
|
||||
// Export the class for initialization
|
||||
export { ClineEndpoint }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { AIhubmixHandler } from "./providers/aihubmix"
|
||||
import { AnthropicHandler } from "./providers/anthropic"
|
||||
@@ -480,7 +481,7 @@ export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): Ap
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("buildApiHandler error:", error)
|
||||
Logger.error("buildApiHandler error:", error)
|
||||
}
|
||||
|
||||
return createHandlerForProvider(apiProvider, options, mode)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { GenerateContentConfig, GoogleGenAI } from "@google/genai"
|
||||
import { ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { sanitizeAnthropicMessages } from "../transform/anthropic-format"
|
||||
@@ -44,6 +45,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
baseURL: this.options.baseURL,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -64,6 +66,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
baseURL: `${this.options.baseURL}/v1`,
|
||||
defaultHeaders: {
|
||||
"APP-Code": this.options.appCode,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -87,6 +90,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
// @ts-expect-error
|
||||
"APP-Code": this.options.appCode,
|
||||
Authorization: `Bearer ${this.options.apiKey ?? ""}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
@@ -33,6 +34,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -58,7 +60,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
private apiKey: string
|
||||
|
||||
constructor(options: AskSageHandlerOptions) {
|
||||
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
Logger.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
this.options = options
|
||||
this.apiKey = options.asksageApiKey || ""
|
||||
this.apiUrl = options.asksageApiUrl || askSageDefaultURL
|
||||
@@ -95,10 +97,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
// Make request to AskSage API
|
||||
const response = await fetch(`${this.apiUrl}/query`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
|
||||
@@ -156,15 +155,12 @@ export class AskSageHandler implements ApiHandler {
|
||||
try {
|
||||
const response = await fetch(`${this.apiUrl}/count-monthly-tokens`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ app_name: "asksage" }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch AskSage usage", await response.text())
|
||||
Logger.error("Failed to fetch AskSage usage", await response.text())
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -177,7 +173,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
outputTokens: 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching AskSage usage:", error)
|
||||
Logger.error("Error fetching AskSage usage:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -193,4 +189,12 @@ export class AskSageHandler implements ApiHandler {
|
||||
info: askSageModels[askSageDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private headers() {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
...buildExternalBasicHeaders(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BasetenModelId, basetenDefaultModelId, basetenModels, ModelInfo } from
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -34,6 +35,7 @@ export class BasetenHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://inference.baseten.co/v1",
|
||||
apiKey: this.options.basetenApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_
|
||||
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@utils/cost"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
@@ -109,9 +110,13 @@ interface ProviderChainOptions {
|
||||
profile?: string
|
||||
}
|
||||
|
||||
// a special jp inference profile was created for sonnet 4.5
|
||||
// a special jp inference profile was created for sonnet 4.5 & haiku 4.5
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html
|
||||
const JP_SUPPORTED_CRIS_MODELS = ["anthropic.claude-sonnet-4-5-20250929-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0:1m"]
|
||||
const JP_SUPPORTED_CRIS_MODELS = [
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0:1m",
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
]
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||
export class AwsBedrockHandler implements ApiHandler {
|
||||
@@ -449,7 +454,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing Deepseek response chunk:", error)
|
||||
Logger.error("Error parsing Deepseek response chunk:", error)
|
||||
// Propagate the error by yielding a text response with error information
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -550,7 +555,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
// Debug logging to see actual response structure
|
||||
// console.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
|
||||
// Handle thinking response in additionalModelResponseFields (LangChain format)
|
||||
const metadata = chunk.metadata as ExtendedMetadata | undefined
|
||||
@@ -854,7 +859,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is ContentBlock => item !== null)
|
||||
@@ -912,7 +917,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
Logger.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
@@ -1111,7 +1116,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with OpenAI model via Converse API:", error)
|
||||
Logger.error("Error with OpenAI model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process OpenAI model request"
|
||||
@@ -1246,7 +1251,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with Qwen model via Converse API:", error)
|
||||
Logger.error("Error with Qwen model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process Qwen model request"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Cerebras from "@cerebras/cerebras_cloud_sdk"
|
||||
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
@@ -35,11 +36,13 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
this.client = new Cerebras({
|
||||
apiKey: cleanApiKey,
|
||||
timeout: 30000, // 30 second timeout
|
||||
fetch, // Use configured fetch with proxy support
|
||||
defaultHeaders: {
|
||||
...externalHeaders,
|
||||
"X-Cerebras-3rd-Party-Integration": "cline",
|
||||
},
|
||||
})
|
||||
@@ -112,10 +115,11 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
const model = this.getModel()
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
model: model.id,
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
temperature: model.info.temperature ?? 0,
|
||||
stream: true,
|
||||
max_tokens: CEREBRAS_DEFAULT_MAX_TOKENS,
|
||||
})
|
||||
|
||||
@@ -7,10 +7,10 @@ import { ClineEnv } from "@/config"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildClineExtraHeaders } from "@/services/EnvUtils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -35,10 +35,13 @@ export class ClineHandler implements ApiHandler {
|
||||
private clineAccountService = ClineAccountService.getInstance()
|
||||
private _authService: AuthService
|
||||
private client: OpenAI | undefined
|
||||
private readonly _baseUrl = ClineEnv.config().apiBaseUrl
|
||||
lastGenerationId?: string
|
||||
private lastRequestId?: string
|
||||
|
||||
private get _baseUrl(): string {
|
||||
return ClineEnv.config().apiBaseUrl
|
||||
}
|
||||
|
||||
constructor(options: ClineHandlerOptions) {
|
||||
this.options = options
|
||||
this._authService = AuthService.getInstance()
|
||||
@@ -126,7 +129,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
Logger.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
@@ -143,7 +146,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
Logger.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -199,7 +202,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
|
||||
if (["kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
@@ -217,14 +220,14 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
// Fallback to generation endpoint if usage chunk not returned
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
Logger.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
const apiStreamUsage = await this.getApiStreamUsage()
|
||||
if (apiStreamUsage) {
|
||||
yield apiStreamUsage
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline API Error:", error)
|
||||
Logger.error("Cline API Error:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -260,7 +263,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching cline generation details:", error)
|
||||
Logger.error("Error fetching cline generation details:", error)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
@@ -2,12 +2,13 @@ import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } fr
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { addReasoningContent } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
@@ -33,6 +34,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
apiKey: this.options.deepSeekApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -81,14 +83,10 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
const convertedMessages = convertToOpenAiMessages(messages)
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = isDeepseekReasoner
|
||||
? [{ role: "system", content: systemPrompt }, ...addReasoningContent(convertedMessages, messages)]
|
||||
: [{ role: "system", content: systemPrompt }, ...convertedMessages]
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ModelInfo } from "../../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -84,7 +86,7 @@ export class DifyHandler implements ApiHandler {
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
console.log("[DIFY DEBUG] Constructor called with:", {
|
||||
Logger.log("[DIFY DEBUG] Constructor called with:", {
|
||||
hasApiKey: !!this.apiKey,
|
||||
baseUrl: this.baseUrl,
|
||||
})
|
||||
@@ -98,7 +100,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
console.log("[DIFY DEBUG] createMessage called with:", {
|
||||
Logger.log("[DIFY DEBUG] createMessage called with:", {
|
||||
systemPromptLength: systemPrompt?.length || 0,
|
||||
messagesCount: messages?.length || 0,
|
||||
})
|
||||
@@ -115,35 +117,32 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const fullUrl = `${this.baseUrl}/chat-messages`
|
||||
console.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(fullUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
|
||||
throw new Error(`Dify API network error: ${error.message}${cause}`)
|
||||
}
|
||||
|
||||
console.log("[DIFY DEBUG] Response status:", response.status)
|
||||
Logger.log("[DIFY DEBUG] Response status:", response.status)
|
||||
const headersObj: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
console.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[DIFY DEBUG] Error response:", errorText)
|
||||
Logger.error("[DIFY DEBUG] Error response:", errorText)
|
||||
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
@@ -159,14 +158,14 @@ export class DifyHandler implements ApiHandler {
|
||||
const processedEvents: string[] = []
|
||||
let lastEventTime = Date.now()
|
||||
|
||||
console.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
console.log("[DIFY DEBUG] Stream ended naturally")
|
||||
console.log(
|
||||
Logger.log("[DIFY DEBUG] Stream ended naturally")
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Final state - hasYieldedContent:",
|
||||
hasYieldedContent,
|
||||
"fullText length:",
|
||||
@@ -178,7 +177,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
|
||||
buffer += chunk
|
||||
const lines = buffer.split("\n")
|
||||
@@ -187,41 +186,41 @@ export class DifyHandler implements ApiHandler {
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim()
|
||||
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
|
||||
if (data === "[DONE]") {
|
||||
console.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
Logger.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
break
|
||||
}
|
||||
|
||||
if (data === "") {
|
||||
console.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
Logger.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "unknown")
|
||||
lastEventTime = Date.now()
|
||||
|
||||
// Capture conversation_id as soon as it's available
|
||||
if (parsed.conversation_id && !this.conversationId) {
|
||||
this.conversationId = parsed.conversation_id
|
||||
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
}
|
||||
|
||||
// Handle different Dify event types based on actual Dify API
|
||||
if (parsed.event === "message") {
|
||||
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
// Dify sends the full text in each "answer" chunk, so we replace.
|
||||
if (typeof parsed.answer === "string") {
|
||||
fullText = parsed.answer
|
||||
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -229,10 +228,10 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_replace") {
|
||||
console.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
if (parsed.answer) {
|
||||
fullText = parsed.answer // Replace instead of append
|
||||
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -240,7 +239,7 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_end") {
|
||||
console.log("[DIFY DEBUG] Message end event", parsed)
|
||||
Logger.log("[DIFY DEBUG] Message end event", parsed)
|
||||
// Message completed. Yield final text if we have any.
|
||||
if (fullText) {
|
||||
yield {
|
||||
@@ -260,19 +259,19 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return // End of stream
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
|
||||
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
|
||||
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "ping") {
|
||||
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
// Ping event, do nothing
|
||||
} else {
|
||||
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
// Try to extract text from other possible fields
|
||||
if (parsed.text) {
|
||||
fullText += parsed.text
|
||||
@@ -299,17 +298,17 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
Logger.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
}
|
||||
} else if (line.trim() !== "") {
|
||||
console.log(
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
|
||||
JSON.stringify(line),
|
||||
)
|
||||
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
|
||||
try {
|
||||
const parsed = JSON.parse(line.trim())
|
||||
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "direct-json")
|
||||
|
||||
// Handle the same event types as above
|
||||
@@ -330,7 +329,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.answer || parsed.text || parsed.content) {
|
||||
// Fallback for any content in direct JSON
|
||||
@@ -344,7 +343,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, continue
|
||||
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,11 +358,11 @@ export class DifyHandler implements ApiHandler {
|
||||
streamDuration: Date.now() - lastEventTime,
|
||||
conversationId: this.conversationId,
|
||||
}
|
||||
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
Logger.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
|
||||
// If we have any accumulated text at all, yield it as a fallback
|
||||
if (fullText.trim()) {
|
||||
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
Logger.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -380,7 +379,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
console.log("[DIFY DEBUG] Stream reader released")
|
||||
Logger.log("[DIFY DEBUG] Stream reader released")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +398,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
// Only prepend the system prompt if it's the very first message of a new conversation.
|
||||
if (!this.conversationId && systemPrompt) {
|
||||
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
return `${systemPrompt}\n\n---\n\n${userQuery}`
|
||||
}
|
||||
|
||||
@@ -437,9 +436,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/files/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
body: formData,
|
||||
})
|
||||
|
||||
@@ -460,10 +457,7 @@ export class DifyHandler implements ApiHandler {
|
||||
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
@@ -493,9 +487,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -530,9 +522,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
headers: this.headers(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -552,10 +542,7 @@ export class DifyHandler implements ApiHandler {
|
||||
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
@@ -586,10 +573,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
@@ -622,10 +606,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/messages/${messageId}/feedbacks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: this.jsonHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
@@ -658,4 +639,19 @@ export class DifyHandler implements ApiHandler {
|
||||
this.conversationId = null
|
||||
this.currentTaskId = null
|
||||
}
|
||||
|
||||
private jsonHeaders() {
|
||||
return {
|
||||
...this.headers(),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
private headers() {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return {
|
||||
...externalHeaders,
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -25,10 +25,9 @@ export class DoubaoHandler implements ApiHandler {
|
||||
throw new Error("Doubao API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Doubao client: ${error.message}`)
|
||||
@@ -80,9 +79,9 @@ export class DoubaoHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -28,10 +28,9 @@ export class FireworksHandler implements ApiHandler {
|
||||
throw new Error("Fireworks API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Fireworks client: ${error.message}`)
|
||||
@@ -88,9 +87,9 @@ export class FireworksHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
ThinkingLevel,
|
||||
} from "@google/genai"
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { RetriableError, withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
@@ -62,6 +64,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
private ensureClient(): GoogleGenAI {
|
||||
if (!this.client) {
|
||||
const options = this.options as GeminiHandlerOptions
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
|
||||
if (options.isVertex) {
|
||||
// Initialize with Vertex AI configuration
|
||||
@@ -73,6 +76,9 @@ export class GeminiHandler implements ApiHandler {
|
||||
vertexai: true,
|
||||
project,
|
||||
location,
|
||||
httpOptions: {
|
||||
headers: externalHeaders,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
|
||||
@@ -84,7 +90,12 @@ export class GeminiHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
this.client = new GoogleGenAI({
|
||||
apiKey: options.geminiApiKey,
|
||||
httpOptions: {
|
||||
headers: externalHeaders,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini client: ${error.message}`)
|
||||
}
|
||||
@@ -332,7 +343,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
})
|
||||
} else {
|
||||
console.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
Logger.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,7 +419,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens ?? 0, cost: cacheReadCost }
|
||||
}
|
||||
|
||||
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
// Logger.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
return totalCost
|
||||
}
|
||||
|
||||
@@ -450,13 +461,13 @@ export class GeminiHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
if (response.totalTokens === undefined) {
|
||||
console.warn("Gemini token counting returned undefined, using fallback")
|
||||
Logger.warn("Gemini token counting returned undefined, using fallback")
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
|
||||
return response.totalTokens
|
||||
} catch (error) {
|
||||
console.warn("Gemini token counting failed, using fallback", error)
|
||||
Logger.warn("Gemini token counting failed, using fallback", error)
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
}
|
||||
@@ -475,7 +486,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
const jsonStr = JSON.stringify(block)
|
||||
return total + jsonStr.length
|
||||
} catch (e) {
|
||||
console.warn("Failed to stringify block for token estimation", e)
|
||||
Logger.warn("Failed to stringify block for token estimation", e)
|
||||
return total
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -103,6 +104,7 @@ export class GroqHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.groq.com/openai/v1",
|
||||
apiKey: this.options.groqApiKey,
|
||||
defaultHeaders: buildExternalBasicHeaders(),
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { hicapModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -29,7 +30,7 @@ export class HicapHandler implements ApiHandler {
|
||||
throw new Error("Model ID is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.hicap.ai/v2/openai",
|
||||
apiKey: this.options.hicapApiKey,
|
||||
defaultHeaders: {
|
||||
@@ -86,9 +87,8 @@ export class HicapHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HuaweiCloudMaasModelId, huaweiCloudMaasDefaultModelId, huaweiCloudMaasM
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -28,10 +28,9 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
throw new Error("Huawei Cloud MaaS API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -32,13 +32,9 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://router.huggingface.co/v1",
|
||||
apiKey: this.options.huggingFaceApiKey,
|
||||
defaultHeaders: {
|
||||
"User-Agent": "Cline/1.0",
|
||||
},
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Hugging Face client: ${error.message}`)
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient, fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
@@ -63,6 +65,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"x-litellm-api-key": apiKey,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -70,13 +73,14 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
...buildExternalBasicHeaders(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -84,12 +88,12 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching LiteLLM model info:", error)
|
||||
Logger.error("Error fetching LiteLLM model info:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -111,10 +115,9 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
throw new Error("LiteLLM API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LiteLLM client: ${error.message}`)
|
||||
@@ -169,7 +172,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error getting LiteLLM model cost info:", error)
|
||||
Logger.warn("Error getting LiteLLM model cost info:", error)
|
||||
}
|
||||
|
||||
// Fallback to zero costs if we can't get the information
|
||||
@@ -200,7 +203,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
return totalCost
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,11 +26,10 @@ export class LmStudioHandler implements ApiHandler {
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
|
||||
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
|
||||
apiKey: "noop",
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LM Studio client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/index"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { MinimaxModelId, ModelInfo, minimaxDefaultModelId, minimaxModels } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
@@ -30,12 +31,14 @@ export class MinimaxHandler implements ApiHandler {
|
||||
throw new Error("MiniMax API key is required")
|
||||
}
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.minimaxApiKey,
|
||||
baseURL:
|
||||
this.options.minimaxApiLine === "china"
|
||||
? "https://api.minimaxi.com/anthropic"
|
||||
: "https://api.minimax.io/anthropic",
|
||||
defaultHeaders: externalHeaders,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { HTTPClient } from "@mistralai/mistralai/lib/http"
|
||||
import { Tool as MistralTool } from "@mistralai/mistralai/models/components/tool"
|
||||
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
@@ -29,6 +30,7 @@ export class MistralHandler implements ApiHandler {
|
||||
throw new Error("Mistral API key is required")
|
||||
}
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
// Create HTTP client with custom fetch for proxy support
|
||||
// The Mistral SDK's HTTPClient passes a Request object to the fetcher,
|
||||
// but we need to extract the URL and init options to pass to our fetch wrapper
|
||||
@@ -37,6 +39,11 @@ export class MistralHandler implements ApiHandler {
|
||||
fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
// Handle both string/URL and Request object inputs
|
||||
if (input instanceof Request) {
|
||||
Object.keys(externalHeaders).forEach((key) => {
|
||||
if (!input.headers.has(key)) {
|
||||
input.headers.set(key, externalHeaders[key])
|
||||
}
|
||||
})
|
||||
return fetch(input.url, {
|
||||
method: input.method,
|
||||
headers: input.headers,
|
||||
@@ -48,7 +55,16 @@ export class MistralHandler implements ApiHandler {
|
||||
...init,
|
||||
} as RequestInit)
|
||||
}
|
||||
return fetch(input, init)
|
||||
|
||||
// Merge external headers with existing headers
|
||||
const mergedInit = {
|
||||
...init,
|
||||
headers: {
|
||||
...externalHeaders,
|
||||
...(init?.headers || {}),
|
||||
},
|
||||
}
|
||||
return fetch(input, mergedInit)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,11 +26,10 @@ export class MoonshotHandler implements ApiHandler {
|
||||
throw new Error("Moonshot API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL:
|
||||
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
|
||||
apiKey: this.options.moonshotApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Moonshot client: ${error.message}`)
|
||||
@@ -52,7 +51,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
temperature: model.info.temperature,
|
||||
max_tokens: model.info.maxTokens,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -26,10 +26,9 @@ export class NebiusHandler implements ApiHandler {
|
||||
throw new Error("Nebius API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://api.studio.nebius.ai/v1",
|
||||
apiKey: this.options.nebiusApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Nebius client: ${error.message}`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ModelInfo, NousResearchModelId, nousResearchDefaultModelId, nousResearchModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -25,7 +26,7 @@ export class NousResearchHandler implements ApiHandler {
|
||||
throw new Error("NousResearch API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: "https://inference-api.nousResearch.com/v1",
|
||||
apiKey: this.options.nousResearchApiKey,
|
||||
})
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
OCI_HEADER_OPC_REQUEST_ID,
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -35,12 +36,14 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
export class OcaHandler implements ApiHandler {
|
||||
protected options: OcaHandlerOptions
|
||||
protected client: OpenAI | undefined
|
||||
protected externalHeaders: Record<string, string> = {}
|
||||
|
||||
constructor(options: OcaHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
protected initializeClient(options: OcaHandlerOptions) {
|
||||
protected initializeClient(options: OcaHandlerOptions): OpenAI {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
return new (class OCIOpenAI extends OpenAI {
|
||||
protected override async prepareOptions(opts: any): Promise<void> {
|
||||
const token = await OcaAuthService.getInstance().getAuthToken()
|
||||
@@ -50,7 +53,7 @@ export class OcaHandler implements ApiHandler {
|
||||
opts.headers ??= {}
|
||||
// OCA Headers
|
||||
const ociHeaders = await createOcaHeaders(token, options.taskId!)
|
||||
opts.headers = { ...opts.headers, ...ociHeaders }
|
||||
opts.headers = { ...opts.headers, ...externalHeaders, ...ociHeaders }
|
||||
Logger.log(`Making request with customer opc-request-id: ${opts.headers?.["opc-request-id"]}`)
|
||||
return super.prepareOptions(opts)
|
||||
}
|
||||
@@ -113,12 +116,13 @@ export class OcaHandler implements ApiHandler {
|
||||
if (!token) {
|
||||
throw new OpenAIError("Unable to handle auth, Oracle Code Assist (OCA) access token is not available")
|
||||
}
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const ociHeaders = await createOcaHeaders(token, this.options.taskId!)
|
||||
Logger.log(`Making calculate cost request with customer opc-request-id: ${ociHeaders["opc-request-id"]}`)
|
||||
try {
|
||||
const response = await fetch(`${client.baseURL}/spend/calculate`, {
|
||||
method: "POST",
|
||||
headers: ociHeaders,
|
||||
headers: { ...externalHeaders, ...ociHeaders },
|
||||
body: JSON.stringify({
|
||||
completion_response: {
|
||||
model: modelId,
|
||||
@@ -134,11 +138,11 @@ export class OcaHandler implements ApiHandler {
|
||||
const data: { cost: number } = await response.json()
|
||||
return data.cost
|
||||
} else {
|
||||
console.error("Error calculating spend:", response.statusText)
|
||||
Logger.error("Error calculating spend:", response.statusText)
|
||||
return undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -313,7 +317,7 @@ export class OcaHandler implements ApiHandler {
|
||||
|
||||
// Convert ChatCompletion tools to Responses API format if provided
|
||||
const responseTools = tools
|
||||
?.filter((tool) => tool.type === "function")
|
||||
?.filter((tool) => tool?.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
import { ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
|
||||
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ollamaBaseUrl?: string
|
||||
@@ -29,14 +33,17 @@ export class OllamaHandler implements ApiHandler {
|
||||
private ensureClient(): Ollama {
|
||||
if (!this.client) {
|
||||
try {
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
const clientOptions: Partial<Config> = {
|
||||
host: this.options.ollamaBaseUrl,
|
||||
fetch,
|
||||
headers: externalHeaders,
|
||||
}
|
||||
|
||||
// Add API key if provided (for Ollama cloud or authenticated instances)
|
||||
if (this.options.ollamaApiKey) {
|
||||
clientOptions.headers = {
|
||||
...clientOptions.headers,
|
||||
Authorization: `Bearer ${this.options.ollamaApiKey}`,
|
||||
}
|
||||
}
|
||||
@@ -50,7 +57,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
@@ -69,20 +76,44 @@ export class OllamaHandler implements ApiHandler {
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum),
|
||||
},
|
||||
tools: tools as any,
|
||||
})
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
// Race the API request against the timeout
|
||||
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.message.content,
|
||||
}
|
||||
Logger.debug("[OllamaHandler] Message Chunk" + JSON.stringify(chunk))
|
||||
|
||||
const delta = chunk.message
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
Logger.debug(`[OllamaHandler] Tool Calls Detected: ${JSON.stringify(delta.tool_calls)}`)
|
||||
yield* toolCallProcessor.processToolCallDeltas(
|
||||
delta.tool_calls?.map((tc, inx) => ({
|
||||
index: inx,
|
||||
id: `ollama-tool-${inx}`,
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments:
|
||||
typeof tc.function.arguments === "string"
|
||||
? tc.function.arguments
|
||||
: JSON.stringify(tc.function.arguments),
|
||||
},
|
||||
type: "function",
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof delta.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
// Handle token usage if available
|
||||
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
|
||||
yield {
|
||||
@@ -93,7 +124,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (streamError: any) {
|
||||
console.error("Error processing Ollama stream:", streamError)
|
||||
Logger.error("Error processing Ollama stream:", streamError)
|
||||
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -107,7 +138,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
const statusCode = error.status || error.statusCode
|
||||
const errorMessage = error.message || "Unknown error"
|
||||
|
||||
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
Logger.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { ModelInfo, OpenAiCodexModelId, openAiCodexDefaultModelId, openAiCodexModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import {
|
||||
ModelInfo,
|
||||
OpenAiCodexModelId,
|
||||
openAiCodexDefaultModelId,
|
||||
openAiCodexModels,
|
||||
} from "@shared/api"
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
@@ -53,7 +49,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private normalizeUsage(usage: any, model: { id: string; info: ModelInfo }): ApiStreamUsageChunk | undefined {
|
||||
if (!usage) return undefined
|
||||
if (!usage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const inputDetails = usage.input_tokens_details ?? usage.prompt_tokens_details
|
||||
|
||||
@@ -90,11 +88,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return out
|
||||
}
|
||||
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: ClineStorageMessage[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
// Reset state for this request
|
||||
@@ -104,9 +98,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
// Get access token from OAuth manager
|
||||
let accessToken = await openAiCodexOAuthManager.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error(
|
||||
"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.",
|
||||
)
|
||||
throw new Error("Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.")
|
||||
}
|
||||
|
||||
// Format conversation for Responses API
|
||||
@@ -166,7 +158,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
// Pass through strict value from tool (MCP/custom tools have strict: false, built-in tools default to true)
|
||||
if (tools && tools.length > 0) {
|
||||
body.tools = tools
|
||||
.filter((tool: any) => tool.type === "function")
|
||||
.filter((tool: any) => tool?.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function",
|
||||
name: tool.function.name,
|
||||
@@ -179,11 +171,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return body
|
||||
}
|
||||
|
||||
private async *executeRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *executeRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
// Create AbortController for cancellation
|
||||
this.abortController = new AbortController()
|
||||
|
||||
@@ -197,6 +185,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
session_id: this.sessionId,
|
||||
"User-Agent": `cline/${process.env.npm_package_version || "1.0.0"} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`,
|
||||
...(accountId ? { "ChatGPT-Account-Id": accountId } : {}),
|
||||
...buildExternalBasicHeaders(),
|
||||
}
|
||||
|
||||
// Try using OpenAI SDK first
|
||||
@@ -237,11 +226,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *makeCodexRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *makeCodexRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
const url = `${CODEX_API_BASE_URL}/responses`
|
||||
|
||||
// Get ChatGPT account ID for organization subscriptions
|
||||
@@ -302,10 +287,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *handleStreamResponse(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
private async *handleStreamResponse(body: ReadableStream<Uint8Array>, model: { id: string; info: ModelInfo }): ApiStream {
|
||||
const reader = body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
@@ -317,7 +299,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split("\n")
|
||||
@@ -380,10 +364,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Handle tool/function call deltas
|
||||
if (
|
||||
event?.type === "response.tool_call_arguments.delta" ||
|
||||
event?.type === "response.function_call_arguments.delta"
|
||||
) {
|
||||
if (event?.type === "response.tool_call_arguments.delta" || event?.type === "response.function_call_arguments.delta") {
|
||||
const callId = event.call_id || event.tool_call_id || event.id || this.pendingToolCallId
|
||||
const name = event.name || event.function_name || this.pendingToolCallName
|
||||
const args = event.delta || event.arguments
|
||||
@@ -484,10 +465,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
getModel(): { id: OpenAiCodexModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
const id =
|
||||
modelId && modelId in openAiCodexModels
|
||||
? (modelId as OpenAiCodexModelId)
|
||||
: openAiCodexDefaultModelId
|
||||
const id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId
|
||||
|
||||
const info: ModelInfo = openAiCodexModels[id]
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isGPT5ModelFamily } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -41,9 +41,8 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
throw new Error("OpenAI API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
apiKey: this.options.openAiNativeApiKey,
|
||||
fetch, // Use configured fetch with proxy support
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenAI client: ${error.message}`)
|
||||
@@ -135,7 +134,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +158,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
|
||||
// Convert ChatCompletion tools to Responses API format if provided
|
||||
const responseTools = tools
|
||||
?.filter((tool) => tool.type === "function")
|
||||
?.filter((tool) => tool?.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
@@ -316,11 +315,11 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
Logger.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
Logger.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
Logger.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity"
|
||||
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { AzureOpenAI } from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { buildExternalBasicHeaders } from "@/services/EnvUtils"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { createOpenAIClient, fetch } from "@/shared/net"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -45,6 +46,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
try {
|
||||
const baseUrl = this.options.openAiBaseUrl?.toLowerCase() ?? ""
|
||||
const isAzureDomain = baseUrl.includes("azure.com") || baseUrl.includes("azure.us")
|
||||
const externalHeaders = buildExternalBasicHeaders()
|
||||
// Azure API shape slightly differs from the core API shape...
|
||||
if (
|
||||
this.options.azureApiVersion ||
|
||||
@@ -58,7 +60,10 @@ export class OpenAiHandler implements ApiHandler {
|
||||
this.getAzureAudienceScope(this.options.openAiBaseUrl),
|
||||
),
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
defaultHeaders: {
|
||||
...externalHeaders,
|
||||
...this.options.openAiHeaders,
|
||||
},
|
||||
fetch,
|
||||
})
|
||||
} else {
|
||||
@@ -66,16 +71,18 @@ export class OpenAiHandler implements ApiHandler {
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
defaultHeaders: {
|
||||
...externalHeaders,
|
||||
...this.options.openAiHeaders,
|
||||
},
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
this.client = createOpenAIClient({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
fetch,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -163,7 +170,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
|
||||