feat: introduce provider abstraction for multi-LLM support (Closes #25) - #105
feat: introduce provider abstraction for multi-LLM support (Closes #25)#105laurentketterle-hub wants to merge 10 commits into
Conversation
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
|
Warning Review limit reached
Next review available in: 53 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThe change introduces a shared provider interface, an Anthropic Messages API provider, provider registration and selection, and an optional OpenAI placeholder adapter. ChangesProvider abstraction and implementations
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant ProviderRegistry
participant AnthropicProvider
participant AnthropicMessagesAPI
ProviderRegistry->>AnthropicProvider: select active provider
AnthropicProvider->>AnthropicMessagesAPI: send capability-specific request
AnthropicMessagesAPI-->>AnthropicProvider: return message and usage
AnthropicProvider-->>ProviderRegistry: return normalized response
AnthropicProvider->>AnthropicMessagesAPI: retry transient failure or call Haiku fallback
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/agents/providers/anthropic.js`:
- Around line 4-113: Run the repository’s configured Prettier formatter on the
AnthropicProvider implementation, including MODEL_MAP, helper functions,
_sendMessage, _call, and the public capability methods, then retain the
formatter’s output without changing behavior.
- Around line 101-107: Update analysis() to invoke the Haiku fallback only for
recoverable transient failures or the explicitly supported primary-model
availability error; re-throw authentication, invalid-request, and other
non-recoverable errors immediately, preserving their original context. Keep the
existing fallback request and options unchanged for eligible failures.
- Around line 107-110: Update the fallback path around _sendMessage to record
start before awaiting the request, so latencyMs measures the full fallback
duration. Include the same normalized usage fields returned by _call in the
fallback response, while preserving its existing content, model, provider, and
fallbackUsed values.
- Around line 48-53: Update healthCheck() to use _sendMessage() instead of
calling client.messages.create() directly, passing maxRetries: 0 and applying
the configured requestTimeoutMs through the shared timeout/cancellation path.
Preserve the existing false result for missing credentials, unavailable clients,
and any request failure, while returning true only after the health-check
request succeeds.
- Around line 34-36: Update the constructor’s requestTimeoutMs, maxRetries, and
retryBaseDelayMs assignments to use nullish defaults instead of truthiness-based
fallbacks, preserving explicit zero values such as maxRetries: 0 while still
applying defaults for null or undefined configuration.
- Around line 17-21: Update withTimeout and its caller _sendMessage to create a
per-call AbortController, pass its signal to
this.client.messages.create(payload), abort the request when the timeout fires,
and clear the timeout once the race settles. Preserve the existing LLM_TIMEOUT
error code and status while ensuring retries do not leave the prior SDK request
running.
In `@src/agents/providers/interface.js`:
- Around line 5-12: Update the provider contract methods research, summary,
analysis, and code to satisfy Prettier formatting, and rename their
intentionally unused input and options parameters to _input and _options.
Preserve the existing not-implemented behavior, and apply the required
formatting consistently across these methods.
In `@src/agents/providers/openai-placeholder.js`:
- Around line 6-19: Format src/agents/providers/openai-placeholder.js lines 6-19
and rename the intentionally unused i and o parameters in OpenAIProvider methods
research, summary, analysis, and code to the repository’s underscore-prefixed
form. Apply the repository formatter to src/agents/providers/registry.js lines
8-39; no other behavioral changes are required.
- Around line 12-17: Mark OpenAIProvider unavailable until request execution is
implemented: update healthCheck/capabilities in openai-placeholder.js so it
cannot be selected, while preserving any separate discovery status if needed. In
src/agents/providers/registry.js lines 17-18, exclude the placeholder from
active-provider registration when openaiApiKey exists; both sites must prevent
callers from selecting an adapter whose operations throw Not implemented.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ba0572cb-9183-4643-bbb9-318bada0a7e9
📒 Files selected for processing (4)
src/agents/providers/anthropic.jssrc/agents/providers/interface.jssrc/agents/providers/openai-placeholder.jssrc/agents/providers/registry.js
| import Anthropic from "@anthropic-ai/sdk" | ||
| import { ProviderInterface } from "./interface.js" | ||
|
|
||
| const MODEL_MAP = { | ||
| research: "claude-haiku-4-5-20251001", | ||
| summary: "claude-haiku-4-5-20251001", | ||
| analysisPrimary: "claude-sonnet-4-5-20250929", | ||
| analysisFallback: "claude-haiku-4-5-20251001", | ||
| code: "claude-haiku-4-5-20251001", | ||
| } | ||
|
|
||
| function sleep(ms) { return new Promise(r => setTimeout(r, ms)) } | ||
|
|
||
| function withTimeout(promise, timeoutMs) { | ||
| return Promise.race([promise, new Promise((_, reject) => | ||
| setTimeout(() => { const e = new Error("LLM timeout"); e.code="LLM_TIMEOUT"; e.status=408; reject(e) }, timeoutMs) | ||
| )]) | ||
| } | ||
|
|
||
| function isTransient(err) { | ||
| const s = err?.status | ||
| if ([408,409,429,500,502,503,504].includes(s)) return true | ||
| const m = String(err?.message||"").toLowerCase() | ||
| return m.includes("timeout")||m.includes("temporar")||m.includes("rate limit")||m.includes("overloaded")||m.includes("network") | ||
| } | ||
|
|
||
| export class AnthropicProvider extends ProviderInterface { | ||
| constructor(config={}) { | ||
| super() | ||
| this.apiKey = config.apiKey || "" | ||
| this.requestTimeoutMs = config.requestTimeoutMs || 20000 | ||
| this.maxRetries = config.maxRetries || 2 | ||
| this.retryBaseDelayMs = config.retryBaseDelayMs || 500 | ||
| this._client = null | ||
| } | ||
|
|
||
| get client() { | ||
| if (!this._client && this.apiKey) this._client = new Anthropic({ apiKey: this.apiKey }) | ||
| return this._client | ||
| } | ||
|
|
||
| get name() { return "anthropic" } | ||
| get capabilities() { return ["research","summary","analysis","code"] } | ||
|
|
||
| async healthCheck() { | ||
| try { | ||
| if (!this.apiKey || !this.client) return false | ||
| await this.client.messages.create({ model: MODEL_MAP.research, max_tokens: 1, messages: [{role:"user",content:"ping"}] }) | ||
| return true | ||
| } catch { return false } | ||
| } | ||
|
|
||
| _buildPayload(capability, input) { | ||
| const systems = { | ||
| research: "You are a research assistant. Provide thorough, well-organized analysis.", | ||
| summary: "You are a summarization expert. Condense text into key insights.", | ||
| analysis: "You are a strategic analyst. Provide deep, structured analysis.", | ||
| code: "You are a senior software engineer. Write clean, production-ready code.", | ||
| } | ||
| return { | ||
| model: capability==="analysis" ? MODEL_MAP.analysisPrimary : MODEL_MAP[capability]||MODEL_MAP.research, | ||
| max_tokens: capability==="analysis" ? 4096 : 2048, | ||
| system: systems[capability]||systems.research, | ||
| messages: [{ role: "user", content: input }], | ||
| } | ||
| } | ||
|
|
||
| async _sendMessage(payload, options={}) { | ||
| const tMs = options.timeoutMs ?? this.requestTimeoutMs | ||
| const maxR = options.maxRetries ?? this.maxRetries | ||
| const baseD = options.baseDelayMs ?? this.retryBaseDelayMs | ||
| let attempt = 0 | ||
| while (true) { | ||
| try { return await withTimeout(this.client.messages.create(payload), tMs) } | ||
| catch(err) { | ||
| if (!isTransient(err)) throw err | ||
| if (attempt >= maxR) throw err | ||
| const d = baseD * 2**attempt | ||
| console.warn("Anthropic retry", {attempt: attempt+1, delayMs: d}) | ||
| await sleep(d) | ||
| attempt++ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async _call(capability, input, options) { | ||
| const start = Date.now() | ||
| const r = await this._sendMessage(this._buildPayload(capability, input), options) | ||
| return { content: r.content[0]?.text||"", model: r.model, provider: "anthropic", | ||
| latencyMs: Date.now()-start, | ||
| usage: r.usage ? { inputTokens: r.usage.input_tokens, outputTokens: r.usage.output_tokens } : undefined } | ||
| } | ||
|
|
||
| async research(input, options) { return this._call("research", input, options) } | ||
| async summary(input, options) { return this._call("summary", input, options) } | ||
| async code(input, options) { return this._call("code", input, options) } | ||
|
|
||
| async analysis(input, options) { | ||
| try { return await this._call("analysis", input, options) } | ||
| catch(err) { | ||
| console.warn("Analysis fallback to Haiku:", err.message) | ||
| const fb = { model: MODEL_MAP.analysisFallback, max_tokens: 2048, | ||
| system: "You are a strategic analyst.", messages: [{role:"user",content:input}] } | ||
| const r = await this._sendMessage(fb, {...options, maxRetries:0}) | ||
| const start = Date.now() | ||
| return { content: r.content[0]?.text||"", model: r.model, provider: "anthropic", | ||
| latencyMs: Date.now()-start, fallbackUsed: true } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Restore lint compliance before merge.
The lint pipeline fails because this file does not match the configured Prettier format. Run the repository formatter on this file and commit the result.
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 14-14: Avoid using the initial state variable in setState
Context: setTimeout(r, ms)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
[warning] 18-18: Avoid using the initial state variable in setState
Context: setTimeout(() => { const e = new Error("LLM timeout"); e.code="LLM_TIMEOUT"; e.status=408; reject(e) }, timeoutMs)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🪛 ESLint
[error] 4-4: Replace "@anthropic-ai/sdk" with '@anthropic-ai/sdk'
(prettier/prettier)
[error] 5-5: Replace "./interface.js" with './interface.js'
(prettier/prettier)
[error] 8-8: Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
(prettier/prettier)
[error] 9-9: Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
(prettier/prettier)
[error] 10-10: Replace "claude-sonnet-4-5-20250929" with 'claude-sonnet-4-5-20250929'
(prettier/prettier)
[error] 11-11: Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
(prettier/prettier)
[error] 12-12: Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
(prettier/prettier)
[error] 15-15: Replace ·return·new·Promise(r·=>·setTimeout(r,·ms))· with ⏎··return·new·Promise((r)·=>·setTimeout(r,·ms))⏎
(prettier/prettier)
[error] 18-18: Replace promise, with ⏎····promise,⏎···
(prettier/prettier)
[error] 19-19: Replace ····setTimeout(()·=>·{·const·e·=·new·Error("LLM·timeout");·e.code="LLM_TIMEOUT";·e.status=408;·reject(e) with ······setTimeout(()·=>·{⏎········const·e·=·new·Error('LLM·timeout')⏎········e.code·=·'LLM_TIMEOUT'⏎········e.status·=·408⏎········reject(e)⏎·····
(prettier/prettier)
[error] 20-20: Replace ) with ··),⏎··
(prettier/prettier)
[error] 25-25: Replace 409,429,500,502,503, with ·409,·429,·500,·502,·503,·
(prettier/prettier)
[error] 26-26: Replace ||"" with ·||·''
(prettier/prettier)
[error] 27-27: Replace m.includes("timeout")||m.includes("temporar")||m.includes("rate·limit")||m.includes("overloaded")||m.includes("network" with (⏎····m.includes('timeout')·||⏎····m.includes('temporar')·||⏎····m.includes('rate·limit')·||⏎····m.includes('overloaded')·||⏎····m.includes('network')⏎··
(prettier/prettier)
[error] 31-31: Replace = with ·=·
(prettier/prettier)
[error] 33-33: Replace "" with ''
(prettier/prettier)
[error] 45-45: Replace ·return·"anthropic" with ⏎····return·'anthropic'⏎·
(prettier/prettier)
[error] 46-46: Replace ·return·["research","summary","analysis","code"] with ⏎····return·['research',·'summary',·'analysis',·'code']⏎·
(prettier/prettier)
[error] 51-51: Replace ·model:·MODEL_MAP.research,·max_tokens:·1,·messages:·[{role:"user",content:"ping"}] with ⏎········model:·MODEL_MAP.research,⏎········max_tokens:·1,⏎········messages:·[{·role:·'user',·content:·'ping'·}],⏎·····
(prettier/prettier)
[error] 53-53: Replace ·return·false with ⏎······return·false⏎···
(prettier/prettier)
[error] 58-58: Replace "You·are·a·research·assistant.·Provide·thorough,·well-organized·analysis." with 'You·are·a·research·assistant.·Provide·thorough,·well-organized·analysis.'
(prettier/prettier)
[error] 59-59: Replace "You·are·a·summarization·expert.·Condense·text·into·key·insights." with 'You·are·a·summarization·expert.·Condense·text·into·key·insights.'
(prettier/prettier)
[error] 60-60: Replace "You·are·a·strategic·analyst.·Provide·deep,·structured·analysis." with 'You·are·a·strategic·analyst.·Provide·deep,·structured·analysis.'
(prettier/prettier)
[error] 61-61: Replace "You·are·a·senior·software·engineer.·Write·clean,·production-ready·code." with 'You·are·a·senior·software·engineer.·Write·clean,·production-ready·code.'
(prettier/prettier)
[error] 64-64: Replace ·capability==="analysis"·?·MODEL_MAP.analysisPrimary·:·MODEL_MAP[capability]|| with ⏎········capability·===·'analysis'⏎··········?·MODEL_MAP.analysisPrimary⏎··········:·MODEL_MAP[capability]·||·
(prettier/prettier)
[error] 65-65: Replace ==="analysis" with ·===·'analysis'
(prettier/prettier)
[error] 66-66: Replace || with ·||·
(prettier/prettier)
[error] 67-67: Replace "user" with 'user'
(prettier/prettier)
[error] 71-71: Replace = with ·=·
(prettier/prettier)
[error] 77-77: Replace ·return·await·withTimeout(this.client.messages.create(payload),·tMs)·} with ⏎········return·await·withTimeout(this.client.messages.create(payload),·tMs)
(prettier/prettier)
[error] 78-78: Replace catch with }·catch·
(prettier/prettier)
[error] 81-81: Replace ** with ·**·
(prettier/prettier)
[error] 82-82: Replace "Anthropic·retry",·{attempt:·attempt+1,·delayMs:·d with 'Anthropic·retry',·{·attempt:·attempt·+·1,·delayMs:·d·
(prettier/prettier)
[error] 92-92: Replace ·content:·r.content[0]?.text||"",·model:·r.model,·provider:·"anthropic" with ⏎······content:·r.content[0]?.text·||·'',⏎······model:·r.model,⏎······provider:·'anthropic'
(prettier/prettier)
[error] 93-93: Replace - with ·-·
(prettier/prettier)
[error] 94-94: Replace ·?·{·inputTokens:·r.usage.input_tokens,·outputTokens:·r.usage.output_tokens·}·:·undefined with ⏎········?·{·inputTokens:·r.usage.input_tokens,·outputTokens:·r.usage.output_tokens·}⏎········:·undefined,⏎···
(prettier/prettier)
[error] 97-97: Replace ·return·this._call("research",·input,·options) with ⏎····return·this._call('research',·input,·options)⏎·
(prettier/prettier)
[error] 98-98: Replace ·return·this._call("summary",·input,·options) with ⏎····return·this._call('summary',·input,·options)⏎·
(prettier/prettier)
[error] 99-99: Replace ·return·this._call("code",·input,·options) with ⏎····return·this._call('code',·input,·options)⏎·
(prettier/prettier)
[error] 102-102: Replace ·return·await·this._call("analysis",·input,·options)·} with ⏎······return·await·this._call('analysis',·input,·options)
(prettier/prettier)
[error] 103-103: Replace ·catch with ·}·catch·
(prettier/prettier)
[error] 104-104: Replace "Analysis·fallback·to·Haiku:" with 'Analysis·fallback·to·Haiku:'
(prettier/prettier)
[error] 105-105: Replace ·model:·MODEL_MAP.analysisFallback, with ⏎········model:·MODEL_MAP.analysisFallback,⏎·······
(prettier/prettier)
[error] 106-106: Replace "You·are·a·strategic·analyst.",·messages:·[{role:"user",content:input}] with 'You·are·a·strategic·analyst.',⏎········messages:·[{·role:·'user',·content:·input·}],⏎·····
(prettier/prettier)
[error] 107-107: Replace ...options,·maxRetries:0 with ·...options,·maxRetries:·0·
(prettier/prettier)
[error] 109-109: Replace ·content:·r.content[0]?.text||"",·model:·r.model,·provider:·"anthropic" with ⏎········content:·r.content[0]?.text·||·'',⏎········model:·r.model,⏎········provider:·'anthropic'
(prettier/prettier)
[error] 110-110: Replace -start,·fallbackUsed:·true with ·-·start,⏎········fallbackUsed:·true,⏎·····
(prettier/prettier)
🪛 GitHub Actions: Lint / 0_lint.txt
[error] 4-4: ESLint Prettier formatting error: replace double quotes around "@anthropic-ai/sdk" with single quotes.
🪛 GitHub Actions: Lint / lint
[error] 4-4: ESLint Prettier formatting error: Replace "@anthropic-ai/sdk" with '@anthropic-ai/sdk'.
🪛 GitHub Check: lint
[failure] 19-19:
Replace ····setTimeout(()·=>·{·const·e·=·new·Error("LLM·timeout");·e.code="LLM_TIMEOUT";·e.status=408;·reject(e) with ······setTimeout(()·=>·{⏎········const·e·=·new·Error('LLM·timeout')⏎········e.code·=·'LLM_TIMEOUT'⏎········e.status·=·408⏎········reject(e)⏎·····
[failure] 18-18:
Replace promise, with ⏎····promise,⏎···
[failure] 15-15:
Replace ·return·new·Promise(r·=>·setTimeout(r,·ms))· with ⏎··return·new·Promise((r)·=>·setTimeout(r,·ms))⏎
[failure] 12-12:
Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
[failure] 11-11:
Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
[failure] 10-10:
Replace "claude-sonnet-4-5-20250929" with 'claude-sonnet-4-5-20250929'
[failure] 9-9:
Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
[failure] 8-8:
Replace "claude-haiku-4-5-20251001" with 'claude-haiku-4-5-20251001'
[failure] 5-5:
Replace "./interface.js" with './interface.js'
[failure] 4-4:
Replace "@anthropic-ai/sdk" with '@anthropic-ai/sdk'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/anthropic.js` around lines 4 - 113, Run the repository’s
configured Prettier formatter on the AnthropicProvider implementation, including
MODEL_MAP, helper functions, _sendMessage, _call, and the public capability
methods, then retain the formatter’s output without changing behavior.
Sources: Linters/SAST tools, Pipeline failures
| function withTimeout(promise, timeoutMs) { | ||
| return Promise.race([promise, new Promise((_, reject) => | ||
| setTimeout(() => { const e = new Error("LLM timeout"); e.code="LLM_TIMEOUT"; e.status=408; reject(e) }, timeoutMs) | ||
| )]) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 2 '"`@anthropic-ai/sdk`"|`@anthropic-ai/sdk`' \
$(fd -a 'package.json' -o -a 'package-lock.json' -o -a 'npm-shrinkwrap.json' -o -a 'pnpm-lock.yaml' -o -a 'yarn.lock')
npm view `@anthropic-ai/sdk`@0.98.0 dist.tarballRepository: Flamki/stellarmind
Length of output: 321
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Lock/package files:\n'
fd -a . | rg '(-lock\.json|package\.json|pnpm-lock\.yaml|yarn\.lock)$' || true
printf '\nAnthropic SDK dependency references:\n'
rg -n -C 2 '"`@anthropic-ai/sdk`"|`@anthropic-ai/sdk`' package.json package-lock.json 2>/dev/null || true
printf '\nAnthropic provider file context:\n'
cat -n src/agents/providers/anthropic.js | sed -n '1,140p'
printf '\nPackage manager availability:\n'
(command -v npm && npm --version) || true
(command -v pnpm && pnpm --version) || true
(command -v yarn && yarn --version) || trueRepository: Flamki/stellarmind
Length of output: 6740
🌐 Web query:
@anthropic-ai/sdk 0.98.0 messages create AbortController cancel documentation
💡 Result:
To cancel a request made via the @anthropic-ai/sdk, you can use the standard AbortController API [1][2]. The method for cancellation depends on whether you are using the streaming or non-streaming API [3][4]. For streaming requests, you can pass an AbortSignal to the messages.create method [2]. If you need to cancel a stream after it has been initiated, you can also call stream.controller.abort or simply break from the async iterator loop [5][6][7]. Example using AbortSignal for streaming: const controller = new AbortController; const stream = await client.messages.create( {...params, stream: true }, { signal: controller.signal }); // To cancel the request: controller.abort; For non-streaming requests, you can also pass an AbortSignal via the request options [1][4]. If the request is aborted, the SDK will typically throw an APIUserAbortError (or a similar error indicating the request was aborted) [1][8][2]. Additionally, when working with frameworks like Express, it is a recommended pattern to bind an AbortController to the request lifecycle (e.g., req.on('close',...)) to ensure upstream streams are torn down immediately if the client disconnects [2].
Citations:
- 1: Cancellation of requests via AbortSignal anthropics/anthropic-sdk-typescript#43
- 2: https://claudelab.net/en/articles/api-sdk/claude-api-streaming-cancellation-abortcontroller-cost-guide
- 3: https://cdn.jsdelivr.net/npm/@anthropic-ai/sdk@0.81.0/resources/messages/messages.d.ts
- 4: https://github.com/anthropics/anthropic-sdk-typescript/blob/0f8153b3/src/resources/messages/messages.ts
- 5: https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript
- 6: https://github.com/anthropics/anthropic-sdk-typescript/blob/0f8153b3/helpers.md
- 7: https://github.com/anthropics/anthropic-sdk-typescript/blob/ccf4cac31381624d965ba32ebb22c1db0b207248/README.md
- 8: https://cdn.jsdelivr.net/npm/@anthropic-ai/sdk@0.80.0/src/client.ts
🌐 Web query:
site:jsdelivr.net/npm/@anthropic-ai/sdk@0.98.0/src @anthropic-ai/sdk 0.98.0 messages create signal
💡 Result:
In the @anthropic-ai/sdk (version 0.98.0), you can pass an AbortSignal to the messages.create method using the optional RequestOptions object as the second argument [1][2][3]. This pattern allows you to cancel ongoing non-streaming or streaming requests if necessary [4][5]. Example usage: const controller = new AbortController; // To cancel, call controller.abort; const message = await client.messages.create( { model: "claude-3-5-sonnet-20241022", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], }, { signal: controller.signal }); For streaming requests via client.messages.stream, the same RequestOptions object structure is used to pass the AbortSignal [5]. Alternatively, the MessageStream object returned by.stream exposes an.abort method directly [6][7].
Citations:
- 1: https://cdn.jsdelivr.net/npm/@anthropic-ai/sdk@0.66.0/src/resources/messages/messages.ts
- 2: https://cdn.jsdelivr.net/npm/@anthropic-ai/sdk@0.81.0/resources/messages/messages.d.ts
- 3: https://deepwiki.com/anthropics/anthropic-sdk-typescript/3.2-messages-api
- 4: Cancellation of requests via AbortSignal anthropics/anthropic-sdk-typescript#43
- 5: https://claudelab.net/en/articles/api-sdk/claude-api-streaming-cancellation-abortcontroller-cost-guide
- 6: https://github.com/anthropics/anthropic-sdk-typescript/blob/0f8153b3/helpers.md
- 7: https://github.com/anthropics/anthropic-sdk-typescript/blob/5ccd74353d14ed78b8085748700602827f9b993c/helpers.md
Cancel timed-out Anthropic requests and clear the timeout timer.
The current Promise.race() only rejects the wrapper; this.client.messages.create(payload) keeps running, and _sendMessage() can retry while the prior request still consumes provider capacity and charges. Resolve this by aborting the SDK request with a per-call AbortSignal and clearing the timer in the settled handler.
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 18-18: Avoid using the initial state variable in setState
Context: setTimeout(() => { const e = new Error("LLM timeout"); e.code="LLM_TIMEOUT"; e.status=408; reject(e) }, timeoutMs)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🪛 ESLint
[error] 18-18: Replace promise, with ⏎····promise,⏎···
(prettier/prettier)
[error] 19-19: Replace ····setTimeout(()·=>·{·const·e·=·new·Error("LLM·timeout");·e.code="LLM_TIMEOUT";·e.status=408;·reject(e) with ······setTimeout(()·=>·{⏎········const·e·=·new·Error('LLM·timeout')⏎········e.code·=·'LLM_TIMEOUT'⏎········e.status·=·408⏎········reject(e)⏎·····
(prettier/prettier)
[error] 20-20: Replace ) with ··),⏎··
(prettier/prettier)
🪛 GitHub Check: lint
[failure] 19-19:
Replace ····setTimeout(()·=>·{·const·e·=·new·Error("LLM·timeout");·e.code="LLM_TIMEOUT";·e.status=408;·reject(e) with ······setTimeout(()·=>·{⏎········const·e·=·new·Error('LLM·timeout')⏎········e.code·=·'LLM_TIMEOUT'⏎········e.status·=·408⏎········reject(e)⏎·····
[failure] 18-18:
Replace promise, with ⏎····promise,⏎···
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/anthropic.js` around lines 17 - 21, Update withTimeout
and its caller _sendMessage to create a per-call AbortController, pass its
signal to this.client.messages.create(payload), abort the request when the
timeout fires, and clear the timeout once the race settles. Preserve the
existing LLM_TIMEOUT error code and status while ensuring retries do not leave
the prior SDK request running.
| this.requestTimeoutMs = config.requestTimeoutMs || 20000 | ||
| this.maxRetries = config.maxRetries || 2 | ||
| this.retryBaseDelayMs = config.retryBaseDelayMs || 500 |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve explicit zero-valued retry configuration.
config.maxRetries || 2 converts maxRetries: 0 to 2. A caller cannot disable retries, even though the fallback path explicitly uses maxRetries: 0. Use nullish defaults for all three settings.
Proposed fix
- this.requestTimeoutMs = config.requestTimeoutMs || 20000
- this.maxRetries = config.maxRetries || 2
- this.retryBaseDelayMs = config.retryBaseDelayMs || 500
+ this.requestTimeoutMs = config.requestTimeoutMs ?? 20000
+ this.maxRetries = config.maxRetries ?? 2
+ this.retryBaseDelayMs = config.retryBaseDelayMs ?? 500📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| this.requestTimeoutMs = config.requestTimeoutMs || 20000 | |
| this.maxRetries = config.maxRetries || 2 | |
| this.retryBaseDelayMs = config.retryBaseDelayMs || 500 | |
| this.requestTimeoutMs = config.requestTimeoutMs ?? 20000 | |
| this.maxRetries = config.maxRetries ?? 2 | |
| this.retryBaseDelayMs = config.retryBaseDelayMs ?? 500 |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/anthropic.js` around lines 34 - 36, Update the
constructor’s requestTimeoutMs, maxRetries, and retryBaseDelayMs assignments to
use nullish defaults instead of truthiness-based fallbacks, preserving explicit
zero values such as maxRetries: 0 while still applying defaults for null or
undefined configuration.
| async healthCheck() { | ||
| try { | ||
| if (!this.apiKey || !this.client) return false | ||
| await this.client.messages.create({ model: MODEL_MAP.research, max_tokens: 1, messages: [{role:"user",content:"ping"}] }) | ||
| return true | ||
| } catch { return false } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Apply the configured timeout to healthCheck().
healthCheck() calls messages.create() directly. It bypasses requestTimeoutMs and the common error handling path. A stalled health check can block provider selection longer than the configured request limit.
Call _sendMessage() with maxRetries: 0 after the timeout cancellation fix.
🧰 Tools
🪛 ESLint
[error] 51-51: Replace ·model:·MODEL_MAP.research,·max_tokens:·1,·messages:·[{role:"user",content:"ping"}] with ⏎········model:·MODEL_MAP.research,⏎········max_tokens:·1,⏎········messages:·[{·role:·'user',·content:·'ping'·}],⏎·····
(prettier/prettier)
[error] 53-53: Replace ·return·false with ⏎······return·false⏎···
(prettier/prettier)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/anthropic.js` around lines 48 - 53, Update healthCheck()
to use _sendMessage() instead of calling client.messages.create() directly,
passing maxRetries: 0 and applying the configured requestTimeoutMs through the
shared timeout/cancellation path. Preserve the existing false result for missing
credentials, unavailable clients, and any request failure, while returning true
only after the health-check request succeeds.
| async analysis(input, options) { | ||
| try { return await this._call("analysis", input, options) } | ||
| catch(err) { | ||
| console.warn("Analysis fallback to Haiku:", err.message) | ||
| const fb = { model: MODEL_MAP.analysisFallback, max_tokens: 2048, | ||
| system: "You are a strategic analyst.", messages: [{role:"user",content:input}] } | ||
| const r = await this._sendMessage(fb, {...options, maxRetries:0}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Restrict fallback to recoverable primary failures.
analysis() falls back after every error. Invalid credentials and invalid requests trigger a second failing request and replace the original error context. Re-throw non-recoverable errors. Only fall back for transient failures or an explicitly supported primary-model availability error.
🧰 Tools
🪛 ESLint
[error] 102-102: Replace ·return·await·this._call("analysis",·input,·options)·} with ⏎······return·await·this._call('analysis',·input,·options)
(prettier/prettier)
[error] 103-103: Replace ·catch with ·}·catch·
(prettier/prettier)
[error] 104-104: Replace "Analysis·fallback·to·Haiku:" with 'Analysis·fallback·to·Haiku:'
(prettier/prettier)
[error] 105-105: Replace ·model:·MODEL_MAP.analysisFallback, with ⏎········model:·MODEL_MAP.analysisFallback,⏎·······
(prettier/prettier)
[error] 106-106: Replace "You·are·a·strategic·analyst.",·messages:·[{role:"user",content:input}] with 'You·are·a·strategic·analyst.',⏎········messages:·[{·role:·'user',·content:·input·}],⏎·····
(prettier/prettier)
[error] 107-107: Replace ...options,·maxRetries:0 with ·...options,·maxRetries:·0·
(prettier/prettier)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/anthropic.js` around lines 101 - 107, Update analysis()
to invoke the Haiku fallback only for recoverable transient failures or the
explicitly supported primary-model availability error; re-throw authentication,
invalid-request, and other non-recoverable errors immediately, preserving their
original context. Keep the existing fallback request and options unchanged for
eligible failures.
| const r = await this._sendMessage(fb, {...options, maxRetries:0}) | ||
| const start = Date.now() | ||
| return { content: r.content[0]?.text||"", model: r.model, provider: "anthropic", | ||
| latencyMs: Date.now()-start, fallbackUsed: true } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Return accurate fallback metrics.
start is set after _sendMessage() completes, so latencyMs excludes the fallback request duration. The fallback response also omits usage, unlike _call(). Start timing before _sendMessage() and return the same normalized usage fields.
🧰 Tools
🪛 ESLint
[error] 107-107: Replace ...options,·maxRetries:0 with ·...options,·maxRetries:·0·
(prettier/prettier)
[error] 109-109: Replace ·content:·r.content[0]?.text||"",·model:·r.model,·provider:·"anthropic" with ⏎········content:·r.content[0]?.text·||·'',⏎········model:·r.model,⏎········provider:·'anthropic'
(prettier/prettier)
[error] 110-110: Replace -start,·fallbackUsed:·true with ·-·start,⏎········fallbackUsed:·true,⏎·····
(prettier/prettier)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/anthropic.js` around lines 107 - 110, Update the
fallback path around _sendMessage to record start before awaiting the request,
so latencyMs measures the full fallback duration. Include the same normalized
usage fields returned by _call in the fallback response, while preserving its
existing content, model, provider, and fallbackUsed values.
| async research(input, options) { throw new Error('Not implemented: research()') } | ||
| async summary(input, options) { throw new Error('Not implemented: summary()') } | ||
| async analysis(input, options) { throw new Error('Not implemented: analysis()') } | ||
| async code(input, options) { throw new Error('Not implemented: code()') } | ||
| get name() { throw new Error('Not implemented: name') } | ||
| async healthCheck() { throw new Error('Not implemented: healthCheck()') } | ||
| get capabilities() { return [] } | ||
| } No newline at end of file |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Restore lint compliance for the provider contract.
These methods fail Prettier checks. The input and options parameters also violate the unused-argument rule. Format the methods and rename intentionally unused parameters to _input and _options.
🧰 Tools
🪛 ESLint
[error] 5-5: Replace ·throw·new·Error('Not·implemented:·research()') with ⏎····throw·new·Error('Not·implemented:·research()')⏎·
(prettier/prettier)
[error] 6-6: Replace ·throw·new·Error('Not·implemented:·summary()') with ⏎····throw·new·Error('Not·implemented:·summary()')⏎·
(prettier/prettier)
[error] 7-7: Replace ·throw·new·Error('Not·implemented:·analysis()') with ⏎····throw·new·Error('Not·implemented:·analysis()')⏎·
(prettier/prettier)
[error] 8-8: Replace ·throw·new·Error('Not·implemented:·code()') with ⏎····throw·new·Error('Not·implemented:·code()')⏎·
(prettier/prettier)
[error] 9-9: Replace ·throw·new·Error('Not·implemented:·name') with ⏎····throw·new·Error('Not·implemented:·name')⏎·
(prettier/prettier)
[error] 10-10: Replace ·throw·new·Error('Not·implemented:·healthCheck()') with ⏎····throw·new·Error('Not·implemented:·healthCheck()')⏎·
(prettier/prettier)
[error] 11-11: Replace ·return·[] with ⏎····return·[]⏎·
(prettier/prettier)
[error] 12-12: Insert ⏎
(prettier/prettier)
🪛 GitHub Check: lint
[warning] 8-8:
'options' is defined but never used. Allowed unused args must match /^_/u
[warning] 8-8:
'input' is defined but never used. Allowed unused args must match /^_/u
[warning] 7-7:
'options' is defined but never used. Allowed unused args must match /^_/u
[warning] 7-7:
'input' is defined but never used. Allowed unused args must match /^_/u
[warning] 6-6:
'options' is defined but never used. Allowed unused args must match /^_/u
[warning] 6-6:
'input' is defined but never used. Allowed unused args must match /^_/u
[warning] 5-5:
'options' is defined but never used. Allowed unused args must match /^_/u
[warning] 5-5:
'input' is defined but never used. Allowed unused args must match /^_/u
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/interface.js` around lines 5 - 12, Update the provider
contract methods research, summary, analysis, and code to satisfy Prettier
formatting, and rename their intentionally unused input and options parameters
to _input and _options. Preserve the existing not-implemented behavior, and
apply the required formatting consistently across these methods.
Source: Linters/SAST tools
| content: 'OpenAI provider is not yet configured. Set OPENAI_API_KEY and install openai npm package.', | ||
| model: 'openai-placeholder', provider: 'openai', latencyMs: 0, | ||
| } | ||
| export class OpenAIProvider extends ProviderInterface { | ||
| constructor(apiKey) { super(); this.apiKey = apiKey||''; this._configured = !!apiKey } | ||
| get name() { return 'openai' } | ||
| get capabilities() { return ['research','summary','analysis','code'] } | ||
| async healthCheck() { return this._configured } | ||
| async research(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } | ||
| async summary(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } | ||
| async analysis(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } | ||
| async code(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } | ||
| } | ||
| export function createOpenAIProvider(apiKey) { return apiKey ? new OpenAIProvider(apiKey) : null } No newline at end of file |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the reported lint errors.
Prettier reports errors in both modules. ESLint also reports unused i and o parameters in the OpenAI provider methods. Apply the repository formatter and rename intentionally unused parameters to the configured underscore form.
src/agents/providers/openai-placeholder.js#L6-L19: format the module and rename unused parameters.src/agents/providers/registry.js#L8-L39: format the module.
🧰 Tools
🪛 ESLint
[error] 6-6: Insert ⏎···
(prettier/prettier)
[error] 7-7: Replace ·provider:·'openai', with ⏎··provider:·'openai',⏎·
(prettier/prettier)
[error] 10-10: Replace ·super();·this.apiKey·=·apiKey||'';·this._configured·=·!!apiKey with ⏎····super()⏎····this.apiKey·=·apiKey·||·''⏎····this._configured·=·!!apiKey⏎·
(prettier/prettier)
[error] 11-11: Replace ·return·'openai' with ⏎····return·'openai'⏎·
(prettier/prettier)
[error] 12-12: Replace ·return·['research','summary','analysis','code'] with ⏎····return·['research',·'summary',·'analysis',·'code']⏎·
(prettier/prettier)
[error] 13-13: Replace ·return·this._configured with ⏎····return·this._configured⏎·
(prettier/prettier)
[error] 14-14: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
[error] 15-15: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
[error] 16-16: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
[error] 17-17: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
[error] 19-19: Replace ·return·apiKey·?·new·OpenAIProvider(apiKey)·:·null·} with ⏎··return·apiKey·?·new·OpenAIProvider(apiKey)·:·null⏎}⏎
(prettier/prettier)
🪛 GitHub Check: lint
[warning] 14-14:
'o' is defined but never used. Allowed unused args must match /^_/u
[warning] 14-14:
'i' is defined but never used. Allowed unused args must match /^_/u
📍 Affects 2 files
src/agents/providers/openai-placeholder.js#L6-L19(this comment)src/agents/providers/registry.js#L8-L39
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/openai-placeholder.js` around lines 6 - 19, Format
src/agents/providers/openai-placeholder.js lines 6-19 and rename the
intentionally unused i and o parameters in OpenAIProvider methods research,
summary, analysis, and code to the repository’s underscore-prefixed form. Apply
the repository formatter to src/agents/providers/registry.js lines 8-39; no
other behavioral changes are required.
Source: Linters/SAST tools
| get capabilities() { return ['research','summary','analysis','code'] } | ||
| async healthCheck() { return this._configured } | ||
| async research(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } | ||
| async summary(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } | ||
| async analysis(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } | ||
| async code(i,o) { return this._configured ? (()=>{throw new Error('Not implemented')})() : PLACEHOLDER } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not register the unimplemented OpenAI adapter as available.
When openaiApiKey exists, initProviders() registers OpenAIProvider. healthCheck() then returns true, and capabilities reports all operations. A caller can select OpenAI, but each operation throws Not implemented.
Exclude this adapter from selectable providers until it can execute requests. If discovery is required, expose its implementation status separately from provider availability.
src/agents/providers/openai-placeholder.js#L12-L17: report unavailable status until request execution exists.src/agents/providers/registry.js#L17-L18: do not register a placeholder as an active-provider candidate.
🧰 Tools
🪛 ESLint
[error] 12-12: Replace ·return·['research','summary','analysis','code'] with ⏎····return·['research',·'summary',·'analysis',·'code']⏎·
(prettier/prettier)
[error] 13-13: Replace ·return·this._configured with ⏎····return·this._configured⏎·
(prettier/prettier)
[error] 14-14: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
[error] 15-15: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
[error] 16-16: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
[error] 17-17: Replace o)·{·return·this._configured·?·(()=>{throw·new·Error('Not·implemented')})()·:·PLACEHOLDER with ·o)·{⏎····return·this._configured⏎······?·(()·=>·{⏎··········throw·new·Error('Not·implemented')⏎········})()⏎······:·PLACEHOLDER⏎·
(prettier/prettier)
🪛 GitHub Check: lint
[warning] 14-14:
'o' is defined but never used. Allowed unused args must match /^_/u
[warning] 14-14:
'i' is defined but never used. Allowed unused args must match /^_/u
📍 Affects 2 files
src/agents/providers/openai-placeholder.js#L12-L17(this comment)src/agents/providers/registry.js#L17-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/agents/providers/openai-placeholder.js` around lines 12 - 17, Mark
OpenAIProvider unavailable until request execution is implemented: update
healthCheck/capabilities in openai-placeholder.js so it cannot be selected,
while preserving any separate discovery status if needed. In
src/agents/providers/registry.js lines 17-18, exclude the placeholder from
active-provider registration when openaiApiKey exists; both sites must prevent
callers from selecting an adapter whose operations throw Not implemented.
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
|
Closed in favor of #108 (cleaner branch with all files properly pushed). Same content, better organization. |
Closes #25
Summary by CodeRabbit