Skip to content

feat: add Requesty as an OpenAI-compatible LLM provider#17

Open
Thibaultjaigu wants to merge 2 commits into
framerslab:masterfrom
Thibaultjaigu:add-requesty-provider
Open

feat: add Requesty as an OpenAI-compatible LLM provider#17
Thibaultjaigu wants to merge 2 commits into
framerslab:masterfrom
Thibaultjaigu:add-requesty-provider

Conversation

@Thibaultjaigu

@Thibaultjaigu Thibaultjaigu commented Jun 23, 2026

Copy link
Copy Markdown

Add Requesty as an OpenAI-compatible LLM provider

This adds a native RequestyProvider to the LLM provider system, mirroring the
existing OpenRouterProvider as closely as possible. Requesty
is an OpenAI-compatible LLM gateway (base URL https://router.requesty.ai/v1)
that exposes models from many upstreams under provider/model naming
(e.g. openai/gpt-4o-mini) — the same aggregator shape AgentOS already supports
for OpenRouter.

What's added

  • src/core/llm/providers/implementations/RequestyProvider.ts — a full
    IProvider implementation, a faithful copy of OpenRouterProvider:
    axios client, RequestyProviderConfig (apiKey, baseURL?, defaultModelId?,
    siteUrl?, appName?, timeouts), default base URL https://router.requesty.ai/v1,
    chat completions (sync + streaming with stream_options.include_usage),
    embeddings, model listing, health check, and clampMaxOutputTokens from
    model-output-limits. Because Requesty is OpenAI-compatible exactly like
    OpenRouter, all request/response/embeddings/error logic is identical. The
    optional HTTP-Referer / X-Title headers (from siteUrl / appName) are
    preserved — Requesty accepts them too.
  • src/core/llm/providers/errors/RequestyProviderError.ts — error class
    mirroring OpenRouterProviderError (same shape, providerId: 'requesty').

Wiring sites

  • AIModelProviderManager.ts: added the RequestyProvider /
    RequestyProviderConfig import, the RequestyProviderConfig member of the
    provider-config union, and a case 'requesty': to the factory switch.
  • structuredOutputFormat.ts: added case 'requesty': to the same branch as
    openrouter — best-effort json_object, since an aggregator may not enforce a
    JSON schema across all upstreams (caller-side Zod validation still runs).
  • model-output-limits.ts: no change needed — the provider/ prefix stripping
    is already generic (/^[^/]+\//) and handles Requesty's provider/model ids
    identically to OpenRouter's.
  • AgentOSConfig.ts: added the REQUESTY_API_KEY env field/passthrough and a
    provider registration block (mirrors the OpenRouter one) so a REQUESTY_API_KEY
    is auto-wired into the manager.

Verification

  • tsc --noEmit (pnpm run typecheck) passes cleanly.
  • Live chat completion against https://router.requesty.ai/v1/chat/completions
    with openai/gpt-4o-mini returned HTTP 200 and a real completion.

Docs: https://requesty.ai · https://docs.requesty.ai

I work at Requesty. This mirrors the existing OpenRouter provider as closely as possible. Happy to adjust or close it if it's not a fit.

Summary by Sourcery

Add Requesty as a first-class OpenAI-compatible LLM provider and wire it into the model provider configuration and structured output handling.

New Features:

  • Introduce a Requesty LLM provider implementation supporting chat completions (sync and streaming), embeddings, model listing, and health checks.
  • Add a Requesty-specific provider error class aligned with existing provider error patterns.
  • Support Requesty configuration via REQUESTY_API_KEY and register it as an auto-wired provider that can become the default when OpenAI/OpenRouter keys are absent.

Enhancements:

  • Extend provider manager configuration and factory to recognize the Requesty provider alongside existing providers.
  • Handle Requesty structured output using the same best-effort json_object strategy as other aggregator-style providers.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Requesty as a new supported LLM provider option. Configure it via REQUESTY_API_KEY. It supports chat completions, streaming responses, embeddings, and model discovery.
    • Requesty can now be used with structured output formatting (best-effort JSON behavior).
  • Bug Fixes

    • Improved streamed response role handling to correctly prefer provided delta roles.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@sourcery-ai

sourcery-ai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new OpenAI-compatible Requesty LLM provider that mirrors the existing OpenRouter provider, wires it into the provider manager and config, and ensures structured output and model-output behavior are consistent with other aggregators.

Flow diagram for Requesty provider wiring and usage

flowchart LR
  Env[REQUESTY_API_KEY in EnvironmentConfig]
  Config[createModelProviderManagerConfig]
  Manager[AIModelProviderManager case 'requesty']
  Provider[RequestyProvider.initialize]
  API[Requesty router.requesty.ai/v1]

  Env --> Config --> Manager --> Provider --> API
Loading

File-Level Changes

Change Details Files
Introduce a Requesty LLM provider implementation that mirrors OpenRouter, including chat (sync + streaming), embeddings, model listing, health checks, token clamping, and error handling.
  • Define RequestyProviderConfig with API key, base URL, default model, site/app metadata, and timeouts, and initialize axios client and ApiKeyPool with appropriate headers and defaults.
  • Implement initialize/shutdown, health check, available-models caching and refresh via /models, and mapping of Requesty model metadata into internal ModelInfo (capabilities, pricing, context window).
  • Implement non-streaming chat completions that map internal options to Requesty/OpenAI parameters, apply clampMaxOutputTokens with a 4096 default, and map Requesty responses (choices, usage, errors) into ModelCompletionResponse.
  • Implement streaming chat completions using SSE with stream_options.include_usage, including chunk parsing, accumulation of tool_calls deltas, correct handling of the final usage-only chunk, abortSignal handling, and mapping to incremental ModelCompletionResponse objects.
  • Implement embeddings endpoint integration, including model capability checks, payload construction from ProviderEmbeddingOptions, and mapping of Requesty embedding responses into ProviderEmbeddingResponse.
  • Implement shared HTTP request helper with robust Axios error handling that decorates messages with HTTP status codes for downstream retry routing, plus an SSE parser that yields lines and converts parsing errors into RequestyProviderError.
src/core/llm/providers/implementations/RequestyProvider.ts
Add a Requesty-specific provider error type for richer error reporting from Requesty API failures.
  • Create RequestyProviderError extending ProviderError, fixing providerId to 'requesty' and adding httpStatus and requestyErrorType fields.
  • Use RequestyProviderError throughout the Requesty provider for initialization, API, and streaming errors, including wrapping low-level Axios errors.
src/core/llm/providers/errors/RequestyProviderError.ts
Wire Requesty into the provider manager, environment configuration, and structured output handling so it can be selected and used like other providers.
  • Extend EnvironmentConfig and getEnvironmentConfig to include REQUESTY_API_KEY and pass it through from process.env.
  • Register a Requesty provider block in createModelProviderManagerConfig that auto-creates a provider entry when REQUESTY_API_KEY is set, sets it as default if OpenAI/OpenRouter are absent, and configures base URL, default model, retries, and timeout.
  • Expand ProviderConfigEntry config union to include RequestyProviderConfig and add a 'requesty' case in AIModelProviderManager's factory switch to instantiate RequestyProvider.
  • Treat 'requesty' the same as 'openrouter' in structuredOutputFormat, degrading to json_object-only structured output with caller-side Zod validation while leaving model-output-limits unchanged (it already handles provider/model IDs generically).
src/core/config/AgentOSConfig.ts
src/core/llm/providers/AIModelProviderManager.ts
src/core/llm/providers/structuredOutputFormat.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The RequestyProviderConfig used in AgentOSConfig.createModelProviderManagerConfig passes maxRetries and timeout, but RequestyProviderConfig only defines requestTimeout/streamRequestTimeout—consider aligning the config shape or mapping those fields explicitly so runtime behavior matches expectations.
  • The RequestyProvider currently logs to console.log/console.warn/console.error; if the rest of the codebase uses a centralized logging utility, it would be more consistent and controllable to route these messages through that instead of raw console calls.
  • In RequestyProvider.initialize, the User-Agent is hardcoded to AgentOS/1.0; consider deriving this from a shared constant or package metadata so it stays in sync with the actual AgentOS version.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `RequestyProviderConfig` used in `AgentOSConfig.createModelProviderManagerConfig` passes `maxRetries` and `timeout`, but `RequestyProviderConfig` only defines `requestTimeout`/`streamRequestTimeout`—consider aligning the config shape or mapping those fields explicitly so runtime behavior matches expectations.
- The `RequestyProvider` currently logs to `console.log`/`console.warn`/`console.error`; if the rest of the codebase uses a centralized logging utility, it would be more consistent and controllable to route these messages through that instead of raw console calls.
- In `RequestyProvider.initialize`, the `User-Agent` is hardcoded to `AgentOS/1.0`; consider deriving this from a shared constant or package metadata so it stays in sync with the actual AgentOS version.

## Individual Comments

### Comment 1
<location path="src/core/llm/providers/implementations/RequestyProvider.ts" line_range="610-611" />
<code_context>
+            costUSD: apiChunk.usage.cost,
+          };
+        }
+        const finalMessage: ChatMessage = {
+          role: choice.delta?.role || accumulatedToolCalls.size > 0 ? 'assistant' : (choice.message?.role || 'assistant'),
+          content: responseTextDelta || (choice.message?.content || null),
+          tool_calls: Array.from(accumulatedToolCalls.values())
</code_context>
<issue_to_address>
**issue (bug_risk):** Operator precedence in role selection is likely wrong and changes intended behavior.

This expression:

```ts
role: choice.delta?.role || accumulatedToolCalls.size > 0
  ? 'assistant'
  : (choice.message?.role || 'assistant'),
```
actually parses as:

```ts
role: (choice.delta?.role || accumulatedToolCalls.size > 0)
  ? 'assistant'
  : (choice.message?.role || 'assistant');
```
so any truthy `choice.delta?.role` (or `accumulatedToolCalls.size > 0`) forces the role to `'assistant'`, and `delta.role` is never surfaced.

If you meant "use `delta.role` if present, else `'assistant'` when there are tool calls, else `message.role || 'assistant'`", please parenthesize explicitly, e.g.:

```ts
role: choice.delta?.role
  ?? (accumulatedToolCalls.size > 0
        ? 'assistant'
        : (choice.message?.role ?? 'assistant')),
```
(or equivalent with extra `()` and `||`).
</issue_to_address>

### Comment 2
<location path="src/core/llm/providers/implementations/RequestyProvider.ts" line_range="347-352" />
<code_context>
+
+    const accumulatedToolCalls: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }> = new Map();
+
+    const abortSignal = options.abortSignal;
+    if (abortSignal?.aborted) {
+      yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted prior to first chunk', type: 'abort' }, isFinal: true };
+      return;
+    }
+    const abortHandler = () => { /* passive; loop logic handles emission */ };
+    abortSignal?.addEventListener('abort', abortHandler, { once: true });
+
</code_context>
<issue_to_address>
**suggestion:** The abort handler is a no-op and only used to add/remove a listener.

Since all abort behavior is handled via `abortSignal?.aborted` checks in the loop, this listener adds no functional value and its callback never produces side effects. Either remove the listener and rely solely on the per-iteration checks, or make the handler meaningful (e.g., set a local flag and break the loop immediately on abort).

Suggested implementation:

```typescript
    const accumulatedToolCalls: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }> = new Map();

    const abortSignal = options.abortSignal;
    if (abortSignal?.aborted) {
      yield {
        id: `requesty-abort-${Date.now()}`,
        object: 'chat.completion.chunk',
        created: Math.floor(Date.now() / 1000),
        modelId,
        choices: [],
        error: { message: 'Stream aborted prior to first chunk', type: 'abort' },
        isFinal: true,
      };
      return;
    }

```

1. Ensure that elsewhere in this streaming method, you continue to check `abortSignal?.aborted` inside the main loop to handle mid-stream aborts.
2. If there is any corresponding `abortSignal?.removeEventListener('abort', abortHandler)` in a `finally` block or cleanup path, remove it as well to avoid referencing the deleted handler.
</issue_to_address>

### Comment 3
<location path="src/core/llm/providers/implementations/RequestyProvider.ts" line_range="122" />
<code_context>
+  public defaultModelId?: string;
+
+  // Corrected: Changed type of this.config to satisfy the Readonly<Required<...>> assignment by providing defaults
+  private config!: Readonly<Required<Omit<RequestyProviderConfig, 'defaultModelId' | 'siteUrl' | 'appName' | 'baseURL' | 'requestTimeout' | 'streamRequestTimeout'>> & RequestyProviderConfig>;
+  private keyPool: ApiKeyPool | null = null;
+  private client!: AxiosInstance;
</code_context>
<issue_to_address>
**issue (complexity):** Consider introducing small helper types and functions (for config, payload building, streaming, tool-call accumulation, and error normalization) to make this provider’s behavior clearer while reducing duplication and inline branching.

You can cut a fair amount of cognitive load here with a few small extra helpers, without changing behavior.

---

### 1. Config typing / initialization

The `Readonly<Required<Omit<...>>> & RequestyProviderConfig` intersection makes it hard to see what the runtime shape is.

You can define a concrete internal type and keep the same defaults/behavior:

```ts
interface InternalRequestyConfig {
  apiKey: string;
  baseURL: string;
  defaultModelId?: string;
  siteUrl?: string;
  appName?: string;
  requestTimeout: number;
  streamRequestTimeout: number;
}

private config!: Readonly<InternalRequestyConfig>;
```

Then in `initialize`:

```ts
public async initialize(config: RequestyProviderConfig): Promise<void> {
  // ... apiKey guard, etc.

  const internalConfig: InternalRequestyConfig = {
    apiKey: config.apiKey,
    baseURL: config.baseURL || 'https://router.requesty.ai/v1',
    defaultModelId: config.defaultModelId,
    siteUrl: config.siteUrl,
    appName: config.appName,
    requestTimeout: config.requestTimeout ?? 60000,
    streamRequestTimeout: config.streamRequestTimeout ?? 180000,
  };

  this.config = Object.freeze(internalConfig);
  this.keyPool = new ApiKeyPool(this.config.apiKey);
  this.defaultModelId = this.config.defaultModelId;
  // ...
}
```

This keeps the runtime shape explicit and removes the `Omit`/`Required` intersection.

---

### 2. Duplicated completion payload construction

`generateCompletion` and `generateCompletionStream` differ only in streaming flags and timeout. You can pull the payload into a shared helper so that max_tokens / tools / response_format etc. live in one place:

```ts
private buildChatCompletionPayload(
  modelId: string,
  messages: ChatMessage[],
  options: ModelCompletionOptions,
  stream: boolean
): Record<string, unknown> {
  const requestyMessages = this.mapToRequestyMessages(messages);

  return {
    model: modelId,
    messages: requestyMessages,
    stream,
    ...(stream && { stream_options: { include_usage: true } }),
    temperature: options.temperature,
    top_p: options.topP,
    max_tokens: clampMaxOutputTokens(modelId, options.maxTokens) ?? 4096,
    presence_penalty: options.presencePenalty,
    frequency_penalty: options.frequencyPenalty,
    stop: options.stopSequences,
    user: options.userId,
    tools: options.tools,
    tool_choice: options.toolChoice,
    ...(options.responseFormat?.type === 'json_object' && {
      response_format: { type: 'json_object' },
    }),
    ...(options.customModelParams || {}),
  };
}
```

Then use it:

```ts
public async generateCompletion(...) {
  this.ensureInitialized();
  const payload = this.buildChatCompletionPayload(modelId, messages, options, false);

  const apiResponseData = await this.makeApiRequest<RequestyChatCompletionAPIResponse>(
    '/chat/completions',
    'POST',
    options.requestTimeout ?? this.config.requestTimeout,
    payload
  );
  return this.mapApiToCompletionResponse(apiResponseData, modelId);
}

public async *generateCompletionStream(...) {
  this.ensureInitialized();
  const payload = this.buildChatCompletionPayload(modelId, messages, options, true);

  const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
    '/chat/completions',
    'POST',
    options.requestTimeout ?? this.config.streamRequestTimeout,
    payload,
    true
  );
  // ...
}
```

---

### 3. Stream chunk handling / SSE loop

`generateCompletionStream` currently mixes abort handling, SSE parsing, DONE detection, JSON parsing, and mapping. That can be moved into a single helper to make the orchestration method easier to scan:

```ts
private async *processCompletionStream(
  stream: NodeJS.ReadableStream,
  modelId: string,
  options: ModelCompletionOptions
): AsyncGenerator<ModelCompletionResponse, void, undefined> {
  const accumulatedToolCalls = new Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }>();
  const abortSignal = options.abortSignal;
  const abortHandler = () => {};
  abortSignal?.addEventListener('abort', abortHandler, { once: true });

  try {
    if (abortSignal?.aborted) {
      yield this.makeAbortChunk(modelId, 'Stream aborted prior to first chunk');
      return;
    }

    for await (const rawChunk of this.parseSseStream(stream)) {
      if (abortSignal?.aborted) {
        yield this.makeAbortChunk(modelId, 'Stream aborted by caller');
        break;
      }
      if (rawChunk === 'data: [DONE]' || (rawChunk.startsWith('data: ') && rawChunk.trim().endsWith('[DONE]'))) {
        break;
      }
      if (!rawChunk.startsWith('data: ')) continue;

      const jsonData = rawChunk.substring('data: '.length);
      try {
        const apiChunk = JSON.parse(jsonData) as RequestyChatCompletionAPIResponse;
        yield this.mapApiToStreamChunkResponse(apiChunk, modelId, accumulatedToolCalls);
      } catch (err) {
        console.warn('RequestyProvider: Failed to parse stream chunk JSON, skipping chunk. Data:', jsonData, 'Error:', err);
      }
    }
  } finally {
    abortSignal?.removeEventListener('abort', abortHandler);
  }
}
```

Then `generateCompletionStream` reduces to:

```ts
public async *generateCompletionStream(...) {
  this.ensureInitialized();
  const payload = this.buildChatCompletionPayload(modelId, messages, options, true);

  const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
    '/chat/completions',
    'POST',
    options.requestTimeout ?? this.config.streamRequestTimeout,
    payload,
    true
  );

  yield* this.processCompletionStream(stream, modelId, options);
}
```

This keeps the happy-path orchestration linear and pushes low-level SSE concerns into one place.

---

### 4. Stream tool-call accumulation

`mapApiToStreamChunkResponse` is doing both tool-call accumulation and final/delta message mapping. Extracting the accumulation into a small helper makes the function easier to reason about:

```ts
private updateToolCalls(
  accumulated: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }>,
  toolCallDeltas: NonNullable<RequestyChatChoice['delta']>['tool_calls']
): ModelCompletionResponse['toolCallsDeltas'] {
  const toolCallsDeltas: NonNullable<ModelCompletionResponse['toolCallsDeltas']> = [];

  for (const tcDelta of toolCallDeltas) {
    let current = accumulated.get(tcDelta.index) ?? { function: { name: '', arguments: '' } };

    if (tcDelta.id) current.id = tcDelta.id;
    if (tcDelta.type) current.type = 'function';
    if (tcDelta.function?.name) current.function!.name += tcDelta.function.name;
    if (tcDelta.function?.arguments) current.function!.arguments += tcDelta.function.arguments;

    accumulated.set(tcDelta.index, current);

    toolCallsDeltas.push({
      index: tcDelta.index,
      id: tcDelta.id,
      type: 'function',
      function: tcDelta.function && {
        name: tcDelta.function.name,
        arguments_delta: tcDelta.function.arguments,
      },
    });
  }

  return toolCallsDeltas;
}
```

Then in `mapApiToStreamChunkResponse`:

```ts
let toolCallsDeltas: ModelCompletionResponse['toolCallsDeltas'];

if (choice.delta?.tool_calls) {
  toolCallsDeltas = this.updateToolCalls(accumulatedToolCalls, choice.delta.tool_calls);
}
```

This pulls the mutation logic out of the already-branchy mapping code.

---

### 5. Error normalization

`makeApiRequest` has substantial error-shaping logic. Pulling that into a helper clarifies the main method:

```ts
private normalizeAxiosError(error: unknown, endpoint: string, body?: Record<string, unknown>) {
  let statusCode: number | undefined;
  let errorData: any;
  let message = 'Unknown Requesty API error';
  let type = 'UNKNOWN_API_ERROR';

  if (axios.isAxiosError(error)) {
    statusCode = error.response?.status;
    errorData = error.response?.data;
    if (errorData?.error && typeof errorData.error === 'object') {
      message = errorData.error.message || message;
      type = errorData.error.type || type;
    } else if (typeof errorData === 'string') {
      message = errorData;
    } else if ((error as Error).message) {
      message = (error as Error).message;
    }
  } else if (error instanceof Error) {
    message = error.message;
  }

  const decoratedMessage = statusCode ? `[${statusCode}] ${message}` : message;

  return {
    statusCode,
    errorData,
    errorType: type,
    decoratedMessage,
    details: {
      requestEndpoint: endpoint,
      requestBodyPreview: body ? JSON.stringify(body).substring(0, 200) + '...' : undefined,
      responseData: errorData,
      underlyingError: error,
    },
  };
}
```

And in `makeApiRequest`:

```ts
} catch (error: unknown) {
  const { statusCode, errorType, decoratedMessage, details } =
    this.normalizeAxiosError(error, endpoint, body);

  throw new RequestyProviderError(
    decoratedMessage,
    'API_REQUEST_FAILED',
    statusCode,
    errorType,
    details
  );
}
```

---

### 6. Non‑null assertions on `c.message`

`mapApiToCompletionResponse` assumes `message` is always present but uses `!`. Either tighten `RequestyChatChoice` for non-stream responses, or add a small guard:

```ts
const choices = apiResponse.choices.map(c => {
  if (!c.message) {
    throw new RequestyProviderError(
      "Received choice without message in non-stream response.",
      "API_RESPONSE_MALFORMED",
      undefined,
      undefined,
      { responseId: apiResponse.id, choiceIndex: c.index }
    );
  }
  return {
    index: c.index,
    message: {
      role: c.message.role,
      content: c.message.content,
      tool_calls: c.message.tool_calls,
    },
    finishReason: c.finish_reason,
    logprobs: c.logprobs,
  };
});
```

Then:

```ts
return {
  id: apiResponse.id,
  object: apiResponse.object,
  created: apiResponse.created,
  modelId: apiResponse.model || requestedModelId,
  choices,
  usage,
};
```

This removes the non-null assertion and makes the assumption explicit.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/core/llm/providers/implementations/RequestyProvider.ts Outdated
Comment on lines +347 to +352
const abortSignal = options.abortSignal;
if (abortSignal?.aborted) {
yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted prior to first chunk', type: 'abort' }, isFinal: true };
return;
}
const abortHandler = () => { /* passive; loop logic handles emission */ };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The abort handler is a no-op and only used to add/remove a listener.

Since all abort behavior is handled via abortSignal?.aborted checks in the loop, this listener adds no functional value and its callback never produces side effects. Either remove the listener and rely solely on the per-iteration checks, or make the handler meaningful (e.g., set a local flag and break the loop immediately on abort).

Suggested implementation:

    const accumulatedToolCalls: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }> = new Map();

    const abortSignal = options.abortSignal;
    if (abortSignal?.aborted) {
      yield {
        id: `requesty-abort-${Date.now()}`,
        object: 'chat.completion.chunk',
        created: Math.floor(Date.now() / 1000),
        modelId,
        choices: [],
        error: { message: 'Stream aborted prior to first chunk', type: 'abort' },
        isFinal: true,
      };
      return;
    }
  1. Ensure that elsewhere in this streaming method, you continue to check abortSignal?.aborted inside the main loop to handle mid-stream aborts.
  2. If there is any corresponding abortSignal?.removeEventListener('abort', abortHandler) in a finally block or cleanup path, remove it as well to avoid referencing the deleted handler.

public defaultModelId?: string;

// Corrected: Changed type of this.config to satisfy the Readonly<Required<...>> assignment by providing defaults
private config!: Readonly<Required<Omit<RequestyProviderConfig, 'defaultModelId' | 'siteUrl' | 'appName' | 'baseURL' | 'requestTimeout' | 'streamRequestTimeout'>> & RequestyProviderConfig>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider introducing small helper types and functions (for config, payload building, streaming, tool-call accumulation, and error normalization) to make this provider’s behavior clearer while reducing duplication and inline branching.

You can cut a fair amount of cognitive load here with a few small extra helpers, without changing behavior.


1. Config typing / initialization

The Readonly<Required<Omit<...>>> & RequestyProviderConfig intersection makes it hard to see what the runtime shape is.

You can define a concrete internal type and keep the same defaults/behavior:

interface InternalRequestyConfig {
  apiKey: string;
  baseURL: string;
  defaultModelId?: string;
  siteUrl?: string;
  appName?: string;
  requestTimeout: number;
  streamRequestTimeout: number;
}

private config!: Readonly<InternalRequestyConfig>;

Then in initialize:

public async initialize(config: RequestyProviderConfig): Promise<void> {
  // ... apiKey guard, etc.

  const internalConfig: InternalRequestyConfig = {
    apiKey: config.apiKey,
    baseURL: config.baseURL || 'https://router.requesty.ai/v1',
    defaultModelId: config.defaultModelId,
    siteUrl: config.siteUrl,
    appName: config.appName,
    requestTimeout: config.requestTimeout ?? 60000,
    streamRequestTimeout: config.streamRequestTimeout ?? 180000,
  };

  this.config = Object.freeze(internalConfig);
  this.keyPool = new ApiKeyPool(this.config.apiKey);
  this.defaultModelId = this.config.defaultModelId;
  // ...
}

This keeps the runtime shape explicit and removes the Omit/Required intersection.


2. Duplicated completion payload construction

generateCompletion and generateCompletionStream differ only in streaming flags and timeout. You can pull the payload into a shared helper so that max_tokens / tools / response_format etc. live in one place:

private buildChatCompletionPayload(
  modelId: string,
  messages: ChatMessage[],
  options: ModelCompletionOptions,
  stream: boolean
): Record<string, unknown> {
  const requestyMessages = this.mapToRequestyMessages(messages);

  return {
    model: modelId,
    messages: requestyMessages,
    stream,
    ...(stream && { stream_options: { include_usage: true } }),
    temperature: options.temperature,
    top_p: options.topP,
    max_tokens: clampMaxOutputTokens(modelId, options.maxTokens) ?? 4096,
    presence_penalty: options.presencePenalty,
    frequency_penalty: options.frequencyPenalty,
    stop: options.stopSequences,
    user: options.userId,
    tools: options.tools,
    tool_choice: options.toolChoice,
    ...(options.responseFormat?.type === 'json_object' && {
      response_format: { type: 'json_object' },
    }),
    ...(options.customModelParams || {}),
  };
}

Then use it:

public async generateCompletion(...) {
  this.ensureInitialized();
  const payload = this.buildChatCompletionPayload(modelId, messages, options, false);

  const apiResponseData = await this.makeApiRequest<RequestyChatCompletionAPIResponse>(
    '/chat/completions',
    'POST',
    options.requestTimeout ?? this.config.requestTimeout,
    payload
  );
  return this.mapApiToCompletionResponse(apiResponseData, modelId);
}

public async *generateCompletionStream(...) {
  this.ensureInitialized();
  const payload = this.buildChatCompletionPayload(modelId, messages, options, true);

  const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
    '/chat/completions',
    'POST',
    options.requestTimeout ?? this.config.streamRequestTimeout,
    payload,
    true
  );
  // ...
}

3. Stream chunk handling / SSE loop

generateCompletionStream currently mixes abort handling, SSE parsing, DONE detection, JSON parsing, and mapping. That can be moved into a single helper to make the orchestration method easier to scan:

private async *processCompletionStream(
  stream: NodeJS.ReadableStream,
  modelId: string,
  options: ModelCompletionOptions
): AsyncGenerator<ModelCompletionResponse, void, undefined> {
  const accumulatedToolCalls = new Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }>();
  const abortSignal = options.abortSignal;
  const abortHandler = () => {};
  abortSignal?.addEventListener('abort', abortHandler, { once: true });

  try {
    if (abortSignal?.aborted) {
      yield this.makeAbortChunk(modelId, 'Stream aborted prior to first chunk');
      return;
    }

    for await (const rawChunk of this.parseSseStream(stream)) {
      if (abortSignal?.aborted) {
        yield this.makeAbortChunk(modelId, 'Stream aborted by caller');
        break;
      }
      if (rawChunk === 'data: [DONE]' || (rawChunk.startsWith('data: ') && rawChunk.trim().endsWith('[DONE]'))) {
        break;
      }
      if (!rawChunk.startsWith('data: ')) continue;

      const jsonData = rawChunk.substring('data: '.length);
      try {
        const apiChunk = JSON.parse(jsonData) as RequestyChatCompletionAPIResponse;
        yield this.mapApiToStreamChunkResponse(apiChunk, modelId, accumulatedToolCalls);
      } catch (err) {
        console.warn('RequestyProvider: Failed to parse stream chunk JSON, skipping chunk. Data:', jsonData, 'Error:', err);
      }
    }
  } finally {
    abortSignal?.removeEventListener('abort', abortHandler);
  }
}

Then generateCompletionStream reduces to:

public async *generateCompletionStream(...) {
  this.ensureInitialized();
  const payload = this.buildChatCompletionPayload(modelId, messages, options, true);

  const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
    '/chat/completions',
    'POST',
    options.requestTimeout ?? this.config.streamRequestTimeout,
    payload,
    true
  );

  yield* this.processCompletionStream(stream, modelId, options);
}

This keeps the happy-path orchestration linear and pushes low-level SSE concerns into one place.


4. Stream tool-call accumulation

mapApiToStreamChunkResponse is doing both tool-call accumulation and final/delta message mapping. Extracting the accumulation into a small helper makes the function easier to reason about:

private updateToolCalls(
  accumulated: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }>,
  toolCallDeltas: NonNullable<RequestyChatChoice['delta']>['tool_calls']
): ModelCompletionResponse['toolCallsDeltas'] {
  const toolCallsDeltas: NonNullable<ModelCompletionResponse['toolCallsDeltas']> = [];

  for (const tcDelta of toolCallDeltas) {
    let current = accumulated.get(tcDelta.index) ?? { function: { name: '', arguments: '' } };

    if (tcDelta.id) current.id = tcDelta.id;
    if (tcDelta.type) current.type = 'function';
    if (tcDelta.function?.name) current.function!.name += tcDelta.function.name;
    if (tcDelta.function?.arguments) current.function!.arguments += tcDelta.function.arguments;

    accumulated.set(tcDelta.index, current);

    toolCallsDeltas.push({
      index: tcDelta.index,
      id: tcDelta.id,
      type: 'function',
      function: tcDelta.function && {
        name: tcDelta.function.name,
        arguments_delta: tcDelta.function.arguments,
      },
    });
  }

  return toolCallsDeltas;
}

Then in mapApiToStreamChunkResponse:

let toolCallsDeltas: ModelCompletionResponse['toolCallsDeltas'];

if (choice.delta?.tool_calls) {
  toolCallsDeltas = this.updateToolCalls(accumulatedToolCalls, choice.delta.tool_calls);
}

This pulls the mutation logic out of the already-branchy mapping code.


5. Error normalization

makeApiRequest has substantial error-shaping logic. Pulling that into a helper clarifies the main method:

private normalizeAxiosError(error: unknown, endpoint: string, body?: Record<string, unknown>) {
  let statusCode: number | undefined;
  let errorData: any;
  let message = 'Unknown Requesty API error';
  let type = 'UNKNOWN_API_ERROR';

  if (axios.isAxiosError(error)) {
    statusCode = error.response?.status;
    errorData = error.response?.data;
    if (errorData?.error && typeof errorData.error === 'object') {
      message = errorData.error.message || message;
      type = errorData.error.type || type;
    } else if (typeof errorData === 'string') {
      message = errorData;
    } else if ((error as Error).message) {
      message = (error as Error).message;
    }
  } else if (error instanceof Error) {
    message = error.message;
  }

  const decoratedMessage = statusCode ? `[${statusCode}] ${message}` : message;

  return {
    statusCode,
    errorData,
    errorType: type,
    decoratedMessage,
    details: {
      requestEndpoint: endpoint,
      requestBodyPreview: body ? JSON.stringify(body).substring(0, 200) + '...' : undefined,
      responseData: errorData,
      underlyingError: error,
    },
  };
}

And in makeApiRequest:

} catch (error: unknown) {
  const { statusCode, errorType, decoratedMessage, details } =
    this.normalizeAxiosError(error, endpoint, body);

  throw new RequestyProviderError(
    decoratedMessage,
    'API_REQUEST_FAILED',
    statusCode,
    errorType,
    details
  );
}

6. Non‑null assertions on c.message

mapApiToCompletionResponse assumes message is always present but uses !. Either tighten RequestyChatChoice for non-stream responses, or add a small guard:

const choices = apiResponse.choices.map(c => {
  if (!c.message) {
    throw new RequestyProviderError(
      "Received choice without message in non-stream response.",
      "API_RESPONSE_MALFORMED",
      undefined,
      undefined,
      { responseId: apiResponse.id, choiceIndex: c.index }
    );
  }
  return {
    index: c.index,
    message: {
      role: c.message.role,
      content: c.message.content,
      tool_calls: c.message.tool_calls,
    },
    finishReason: c.finish_reason,
    logprobs: c.logprobs,
  };
});

Then:

return {
  id: apiResponse.id,
  object: apiResponse.object,
  created: apiResponse.created,
  modelId: apiResponse.model || requestedModelId,
  choices,
  usage,
};

This removes the non-null assertion and makes the assumption explicit.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add Requesty as an OpenAI-compatible LLM provider
✨ Enhancement ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Description

• Add a native Requesty provider supporting chat, streaming, embeddings, and model discovery.
• Wire requesty into the provider manager factory and structured-output response formatting.
• Auto-configure Requesty via REQUESTY_API_KEY with Requesty router defaults.
Diagram

graph TD
  A["AgentOSConfig.ts"] --> B["Provider manager"] --> C["RequestyProvider"] --> D{{"Requesty API"}}
  B --> E["Structured output"]
  C --> F["RequestyProviderError"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Introduce a shared OpenAI-compatible gateway base provider
  • ➕ Eliminates near-duplicate code across OpenRouter/Requesty (SSE parsing, mapping, embeddings, model listing)
  • ➕ Future gateways become small config wrappers (baseURL, headers, providerId)
  • ➕ Bug fixes to streaming/tool-call handling apply everywhere
  • ➖ Requires refactoring existing provider(s), raising regression risk
  • ➖ Larger change set than a straightforward provider addition
2. Parameterize OpenRouterProvider and add Requesty as an alias config
  • ➕ Smallest maintenance footprint: one implementation to maintain
  • ➕ Keeps provider manager wiring minimal (alias mapping)
  • ➖ Provider-specific branding/headers/error typing become conditional logic
  • ➖ Harder to keep provider-specific defaults clear (base URL, header names, timeouts)
3. Use OpenAIProvider with a baseURL override for Requesty
  • ➕ Avoids adding another provider implementation if OpenAIProvider fully suffices
  • ➕ Centralizes OpenAI-compatible behavior in one place
  • ➖ OpenAIProvider behavior (model listing, headers, retry/timeouts, streaming shape) may diverge from aggregator needs
  • ➖ Harder to preserve aggregator-specific headers (HTTP-Referer/X-Title) and error typing cleanly

Recommendation: The PR’s approach (a dedicated RequestyProvider mirroring OpenRouterProvider) is reasonable for a low-risk initial integration and keeps provider-specific behavior isolated. If Requesty support is expected to be long-lived (or more OpenAI-compatible gateways may be added), consider a follow-up refactor to extract a shared OpenAI-compatible gateway base to reduce duplication and ensure future streaming/error fixes propagate consistently.

Files changed (5) +825 / -2

Enhancement (4) +807 / -2
AIModelProviderManager.tsAdd Requesty provider factory wiring +5/-1

Add Requesty provider factory wiring

• Imports 'RequestyProvider' and extends the provider config union to include 'RequestyProviderConfig'. Adds a 'case 'requesty'' branch to instantiate and initialize the provider.

src/core/llm/providers/AIModelProviderManager.ts

RequestyProviderError.tsIntroduce RequestyProviderError type +56/-0

Introduce RequestyProviderError type

• Adds a Requesty-specific ProviderError subclass carrying HTTP status and Requesty error type metadata, with providerId fixed to 'requesty'. Used to standardize error reporting from the Requesty provider.

src/core/llm/providers/errors/RequestyProviderError.ts

RequestyProvider.tsImplement OpenAI-compatible RequestyProvider (chat, stream, embeddings, models) +743/-0

Implement OpenAI-compatible RequestyProvider (chat, stream, embeddings, models)

• Adds a full 'IProvider' implementation targeting Requesty’s OpenAI-compatible router, including model listing and caching, health checks, chat completions (sync + SSE streaming with usage chunks), and embeddings. Clamps/max-defaults 'max_tokens' to avoid credit reservation failures and decorates API errors with HTTP status codes for downstream retry/fallback logic.

src/core/llm/providers/implementations/RequestyProvider.ts

structuredOutputFormat.tsTreat Requesty structured output like other aggregators +3/-1

Treat Requesty structured output like other aggregators

• Adds 'requesty' to the OpenRouter branch so structured output uses best-effort '{ type: 'json_object' }' without provider-enforced schema validation.

src/core/llm/providers/structuredOutputFormat.ts

Other (1) +18 / -0
AgentOSConfig.tsAuto-register Requesty provider via REQUESTY_API_KEY +18/-0

Auto-register Requesty provider via REQUESTY_API_KEY

• Adds 'REQUESTY_API_KEY' to environment config and, when present, registers an enabled 'requesty' provider entry with default Requesty router base URL and model defaults. Sets Requesty as default only when OpenAI and OpenRouter keys are absent.

src/core/config/AgentOSConfig.ts

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c696cf33-bc12-4a08-bbef-71ba06f65be4

📥 Commits

Reviewing files that changed from the base of the PR and between 1817c2c and eb55cf4.

📒 Files selected for processing (2)
  • src/core/llm/providers/implementations/OpenRouterProvider.ts
  • src/core/llm/providers/implementations/RequestyProvider.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/llm/providers/implementations/RequestyProvider.ts

📝 Walkthrough

Walkthrough

Adds a new requesty LLM provider to the system with error handling, full API implementation (completion, streaming, embeddings, model discovery), environment configuration, and provider manager wiring. Also fixes a streaming role selection precedence bug in OpenRouterProvider.

Changes

Requesty LLM Provider Integration

Layer / File(s) Summary
Error class and API type contracts
src/core/llm/providers/errors/RequestyProviderError.ts, src/core/llm/providers/implementations/RequestyProvider.ts
RequestyProviderError extends ProviderError with optional httpStatus and requestyErrorType fields. RequestyProviderConfig interface and all internal Requesty API response, streaming chunk, embedding, and model metadata structures are defined.
Provider initialization, model cache, and message mapping
src/core/llm/providers/implementations/RequestyProvider.ts
RequestyProvider.initialize() validates the API key, builds an Axios client with rotating ApiKeyPool authorization, and populates the model cache from /models. refreshAvailableModels() fetches and maps model metadata via mapApiToModelInfo(). mapToRequestyMessages() converts internal ChatMessage[].
Completion, streaming, embeddings, and model management
src/core/llm/providers/implementations/RequestyProvider.ts
generateCompletion() posts to /chat/completions with token clamping and option mapping. generateCompletionStream() uses SSE with abort signal support and tool-call delta accumulation. generateEmbeddings() validates input and posts to /embeddings. listAvailableModels(), getModelInfo(), checkHealth(), and shutdown() complete the IProvider contract.
Response mappers and HTTP helpers
src/core/llm/providers/implementations/RequestyProvider.ts
mapApiToCompletionResponse() and mapApiToStreamChunkResponse() translate Requesty API payloads into internal ModelCompletionResponse objects, including tool-call delta reconstruction. makeApiRequest() wraps Axios calls and normalizes errors into RequestyProviderError. parseSseStream() buffers a Node readable stream into SSE lines.
Provider registration, config factory, and structured output routing
src/core/llm/providers/AIModelProviderManager.ts, src/core/config/AgentOSConfig.ts, src/core/llm/providers/structuredOutputFormat.ts
AIModelProviderManager imports RequestyProvider, widens the config union type, and adds a requesty switch case. EnvironmentConfig gains REQUESTY_API_KEY; createModelProviderManagerConfig() conditionally registers the provider. buildResponseFormat routes requesty to json_object degradation.

OpenRouter Streaming Role Fix

Layer / File(s) Summary
Streaming final message role selection
src/core/llm/providers/implementations/OpenRouterProvider.ts
The ternary operator in mapApiToStreamChunkResponse now correctly prioritizes choice.delta?.role when present, fixing a precedence bug that could override it with 'assistant'.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A new route through the model maze,
Requesty joins the LLM phase!
SSE streams with deltas bright,
Tool calls reconstructed right.
The rabbit hops — keys rotate true,
Another gateway, fresh and new! 🌐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding Requesty as a new OpenAI-compatible LLM provider to the system.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering what was added, why it was needed, implementation details, and verification steps, though the checklist items are not explicitly marked.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Requesty not env-resolved 🐞 Bug ≡ Correctness
Description
The high-level API provider resolution does not read REQUESTY_API_KEY or provide defaults for
providerId='requesty', so Requesty cannot be selected via env auto-detect or via generateText({
provider: 'requesty' }) without explicit apiKey/model wiring.
Code

src/core/llm/providers/AIModelProviderManager.ts[R130-132]

+          case 'requesty':
+            providerInstance = new RequestyProvider();
+            break;
Evidence
Core manager supports instantiating Requesty, but the API layer that selects providers/models from
env vars and defaults has no Requesty entries, so REQUESTY_API_KEY is ignored and provider defaults
are missing.

src/core/llm/providers/AIModelProviderManager.ts[21-34]
src/core/llm/providers/AIModelProviderManager.ts[115-133]
src/api/model.ts[40-60]
src/api/runtime/provider-defaults.ts[37-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Requesty is wired into the core provider manager, but the public/high-level API resolution layer (`src/api`) still has no knowledge of it. As a result:
- `autoDetectProvider()` will never choose Requesty even when `REQUESTY_API_KEY` is set.
- `resolveProvider('requesty', ...)` will not read `process.env.REQUESTY_API_KEY` because `ENV_KEY_MAP` omits `requesty`.
- `resolveModelOption({ provider: 'requesty' })` will throw because `PROVIDER_DEFAULTS` has no `requesty` entry.

### Issue Context
This PR adds `case 'requesty'` in the core provider factory, but common call paths for end-users go through `src/api/model.ts` + `src/api/runtime/provider-defaults.ts`.

### Fix Focus Areas
- src/api/model.ts[40-60]
- src/api/runtime/provider-defaults.ts[37-142]
- src/api/runtime/__tests__/provider-defaults.test.ts[1-220]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Requesty config keys wrong 🐞 Bug ≡ Correctness
Description
AgentOSConfig registers Requesty with config fields (defaultModel/timeout/maxRetries) that
RequestyProvider does not read (defaultModelId/requestTimeout/streamRequestTimeout), so the
configured default model and timeouts are silently ignored.
Code

src/core/config/AgentOSConfig.ts[R236-249]

+  // Requesty Provider (OpenAI-compatible LLM gateway)
+  if (env.REQUESTY_API_KEY) {
+    providers.push({
+      providerId: 'requesty',
+      enabled: true,
+      isDefault: !env.OPENAI_API_KEY && !env.OPENROUTER_API_KEY, // Default to Requesty if OpenAI/OpenRouter not available
+      config: {
+        apiKey: env.REQUESTY_API_KEY,
+        baseURL: 'https://router.requesty.ai/v1',
+        defaultModel: 'openai/gpt-4o',
+        maxRetries: 3,
+        timeout: 60000,
+      },
+    });
Evidence
The Requesty provider config interface and initialization code only read
defaultModelId/requestTimeout/streamRequestTimeout, but the AgentOSConfig wiring uses different
property names, so those settings never take effect.

src/core/config/AgentOSConfig.ts[236-249]
src/core/llm/providers/implementations/RequestyProvider.ts[31-39]
src/core/llm/providers/implementations/RequestyProvider.ts[138-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`createModelProviderManagerConfig()` configures Requesty using keys that don't match `RequestyProviderConfig`. RequestyProvider reads `defaultModelId`, `requestTimeout`, and `streamRequestTimeout`, but the config block passes `defaultModel`, `timeout`, and `maxRetries`.

### Issue Context
This causes RequestyProvider to initialize with `defaultModelId` unset and to ignore the intended timeout configuration.

### Fix Focus Areas
- src/core/config/AgentOSConfig.ts[236-249]
- src/core/llm/providers/implementations/RequestyProvider.ts[31-39]
- src/core/llm/providers/implementations/RequestyProvider.ts[138-148]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Streaming errors escape generator 🐞 Bug ☼ Reliability
Description
RequestyProvider.generateCompletionStream() can throw if makeApiRequest() fails or parseSseStream()
errors, instead of emitting a final chunk with isFinal:true and an error payload, breaking the
documented streaming semantics consumers rely on.
Code

src/core/llm/providers/implementations/RequestyProvider.ts[R336-384]

+    const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
+      '/chat/completions',
+      'POST',
+      // CR8: honor a per-call requestTimeout override over the stream default.
+      options.requestTimeout ?? this.config.streamRequestTimeout,
+      payload,
+      true
+    );
+
+    const accumulatedToolCalls: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }> = new Map();
+
+    const abortSignal = options.abortSignal;
+    if (abortSignal?.aborted) {
+      yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted prior to first chunk', type: 'abort' }, isFinal: true };
+      return;
+    }
+    const abortHandler = () => { /* passive; loop logic handles emission */ };
+    abortSignal?.addEventListener('abort', abortHandler, { once: true });
+
+    for await (const rawChunk of this.parseSseStream(stream)) {
+      if (abortSignal?.aborted) {
+        yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted by caller', type: 'abort' }, isFinal: true };
+        break;
+      }
+      if (rawChunk.startsWith('data: ') && rawChunk.includes('[DONE]')) {
+        const doneData = rawChunk.substring('data: '.length).trim();
+        if (doneData === '[DONE]') break;
+      }
+      if (rawChunk === 'data: [DONE]') {
+        break;
+      }
+
+      if (rawChunk.startsWith('data: ')) {
+        const jsonData = rawChunk.substring('data: '.length);
+        try {
+          const apiChunk = JSON.parse(jsonData) as RequestyChatCompletionAPIResponse;
+          yield this.mapApiToStreamChunkResponse(apiChunk, modelId, accumulatedToolCalls);
+          // Don't break on finish_reason: with stream_options.include_usage,
+          // Requesty (like OpenAI) emits a trailing usage-only chunk AFTER
+          // the finish_reason chunk and BEFORE [DONE]. Breaking here would
+          // skip the usage chunk and zero out the caller's token totals. The
+          // [DONE] marker check above is the right termination signal.
+        } catch (error: unknown) {
+          console.warn('RequestyProvider: Failed to parse stream chunk JSON, skipping chunk. Data:', jsonData, 'Error:', error);
+        }
+      }
+    }
+    abortSignal?.removeEventListener('abort', abortHandler);
+  }
Evidence
The IProvider contract requires a terminal streamed chunk on error; RequestyProvider's stream
generator has no outer try/catch around the request+SSE loop, so errors propagate as exceptions
instead of final chunks.

src/core/llm/providers/IProvider.ts[19-29]
src/core/llm/providers/implementations/RequestyProvider.ts[336-384]
src/core/llm/providers/implementations/RequestyProvider.ts[711-735]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`generateCompletionStream()` does not wrap request setup and SSE iteration in a try/catch. If `makeApiRequest()` throws (network/auth/etc) or `parseSseStream()` throws while reading, the async generator itself throws and no terminal `ModelCompletionResponse` with `isFinal: true` is emitted.

### Issue Context
The provider contract explicitly states streamed calls MUST emit a terminal chunk with `isFinal: true` even on error, so downstream consumers can always teardown deterministically.

### Fix Focus Areas
- src/core/llm/providers/IProvider.ts[19-29]
- src/core/llm/providers/implementations/RequestyProvider.ts[336-384]
- src/core/llm/providers/implementations/RequestyProvider.ts[711-742]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Wrong role precedence 🐞 Bug ≡ Correctness
Description
The final streamed message role expression mixes || and ?: without parentheses, so it evaluates
as (choice.delta?.role || accumulatedToolCalls.size > 0) ? 'assistant' : ... and ignores an
explicit delta role value.
Code

src/core/llm/providers/implementations/RequestyProvider.ts[R610-612]

+        const finalMessage: ChatMessage = {
+          role: choice.delta?.role || accumulatedToolCalls.size > 0 ? 'assistant' : (choice.message?.role || 'assistant'),
+          content: responseTextDelta || (choice.message?.content || null),
Evidence
The role expression as written is precedence-ambiguous and will not return the delta role string
when present; it instead uses the expression as a boolean guard for the ternary.

src/core/llm/providers/implementations/RequestyProvider.ts[601-621]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The final streamed message role uses `choice.delta?.role || accumulatedToolCalls.size > 0 ? ...` which is parsed as `(a || b) ? ...` due to operator precedence. This can mislabel roles and is not the apparent intent.

### Issue Context
Fix by using explicit parentheses or nullish coalescing, e.g.:
`role: choice.delta?.role ?? (accumulatedToolCalls.size > 0 ? 'assistant' : (choice.message?.role || 'assistant'))`

### Fix Focus Areas
- src/core/llm/providers/implementations/RequestyProvider.ts[610-613]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. ApiKeyPool not rotating 🐞 Bug ☼ Reliability
Description
RequestyProvider creates an ApiKeyPool but only selects a key once during initialize() to build a
static Authorization header, so multi-key rotation and cooldown failover never occurs on subsequent
requests.
Code

src/core/llm/providers/implementations/RequestyProvider.ts[R147-165]

+    this.keyPool = new ApiKeyPool(config.apiKey);
+    this.defaultModelId = this.config.defaultModelId; // Store the potentially undefined value
+
+    const headers: Record<string, string> = {
+      'Authorization': `Bearer ${this.keyPool.next()}`,
+      'Content-Type': 'application/json',
+      'User-Agent': `AgentOS/1.0 (RequestyProvider; ${this.config.appName || 'UnknownApp'})`,
+    };
+    if (this.config.siteUrl) {
+      headers['HTTP-Referer'] = this.config.siteUrl;
+    }
+    if (this.config.appName) {
+      headers['X-Title'] = this.config.appName;
+    }
+
+    this.client = axios.create({
+      baseURL: this.config.baseURL,
+      headers,
+    });
Evidence
ApiKeyPool is explicitly documented as a rotation/failover primitive, but RequestyProvider only
consumes it once at init-time and never reselects a key in makeApiRequest, preventing rotation
across calls.

src/core/providers/ApiKeyPool.ts[4-26]
src/core/llm/providers/implementations/RequestyProvider.ts[147-165]
src/core/llm/providers/implementations/RequestyProvider.ts[660-675]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
ApiKeyPool is intended for automatic multi-key rotation and failover, but RequestyProvider calls `keyPool.next()` only once during initialization and then reuses the same axios client headers for all requests.

### Issue Context
To enable rotation/failover, set the Authorization header per request (e.g., in `makeApiRequest`) using `this.keyPool?.next()`.

### Fix Focus Areas
- src/core/providers/ApiKeyPool.ts[4-26]
- src/core/llm/providers/implementations/RequestyProvider.ts[147-165]
- src/core/llm/providers/implementations/RequestyProvider.ts[660-675]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +130 to +132
case 'requesty':
providerInstance = new RequestyProvider();
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Requesty not env-resolved 🐞 Bug ≡ Correctness

The high-level API provider resolution does not read REQUESTY_API_KEY or provide defaults for
providerId='requesty', so Requesty cannot be selected via env auto-detect or via generateText({
provider: 'requesty' }) without explicit apiKey/model wiring.
Agent Prompt
### Issue description
Requesty is wired into the core provider manager, but the public/high-level API resolution layer (`src/api`) still has no knowledge of it. As a result:
- `autoDetectProvider()` will never choose Requesty even when `REQUESTY_API_KEY` is set.
- `resolveProvider('requesty', ...)` will not read `process.env.REQUESTY_API_KEY` because `ENV_KEY_MAP` omits `requesty`.
- `resolveModelOption({ provider: 'requesty' })` will throw because `PROVIDER_DEFAULTS` has no `requesty` entry.

### Issue Context
This PR adds `case 'requesty'` in the core provider factory, but common call paths for end-users go through `src/api/model.ts` + `src/api/runtime/provider-defaults.ts`.

### Fix Focus Areas
- src/api/model.ts[40-60]
- src/api/runtime/provider-defaults.ts[37-142]
- src/api/runtime/__tests__/provider-defaults.test.ts[1-220]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +236 to +249
// Requesty Provider (OpenAI-compatible LLM gateway)
if (env.REQUESTY_API_KEY) {
providers.push({
providerId: 'requesty',
enabled: true,
isDefault: !env.OPENAI_API_KEY && !env.OPENROUTER_API_KEY, // Default to Requesty if OpenAI/OpenRouter not available
config: {
apiKey: env.REQUESTY_API_KEY,
baseURL: 'https://router.requesty.ai/v1',
defaultModel: 'openai/gpt-4o',
maxRetries: 3,
timeout: 60000,
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Requesty config keys wrong 🐞 Bug ≡ Correctness

AgentOSConfig registers Requesty with config fields (defaultModel/timeout/maxRetries) that
RequestyProvider does not read (defaultModelId/requestTimeout/streamRequestTimeout), so the
configured default model and timeouts are silently ignored.
Agent Prompt
### Issue description
`createModelProviderManagerConfig()` configures Requesty using keys that don't match `RequestyProviderConfig`. RequestyProvider reads `defaultModelId`, `requestTimeout`, and `streamRequestTimeout`, but the config block passes `defaultModel`, `timeout`, and `maxRetries`.

### Issue Context
This causes RequestyProvider to initialize with `defaultModelId` unset and to ignore the intended timeout configuration.

### Fix Focus Areas
- src/core/config/AgentOSConfig.ts[236-249]
- src/core/llm/providers/implementations/RequestyProvider.ts[31-39]
- src/core/llm/providers/implementations/RequestyProvider.ts[138-148]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +336 to +384
const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
'/chat/completions',
'POST',
// CR8: honor a per-call requestTimeout override over the stream default.
options.requestTimeout ?? this.config.streamRequestTimeout,
payload,
true
);

const accumulatedToolCalls: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }> = new Map();

const abortSignal = options.abortSignal;
if (abortSignal?.aborted) {
yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted prior to first chunk', type: 'abort' }, isFinal: true };
return;
}
const abortHandler = () => { /* passive; loop logic handles emission */ };
abortSignal?.addEventListener('abort', abortHandler, { once: true });

for await (const rawChunk of this.parseSseStream(stream)) {
if (abortSignal?.aborted) {
yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted by caller', type: 'abort' }, isFinal: true };
break;
}
if (rawChunk.startsWith('data: ') && rawChunk.includes('[DONE]')) {
const doneData = rawChunk.substring('data: '.length).trim();
if (doneData === '[DONE]') break;
}
if (rawChunk === 'data: [DONE]') {
break;
}

if (rawChunk.startsWith('data: ')) {
const jsonData = rawChunk.substring('data: '.length);
try {
const apiChunk = JSON.parse(jsonData) as RequestyChatCompletionAPIResponse;
yield this.mapApiToStreamChunkResponse(apiChunk, modelId, accumulatedToolCalls);
// Don't break on finish_reason: with stream_options.include_usage,
// Requesty (like OpenAI) emits a trailing usage-only chunk AFTER
// the finish_reason chunk and BEFORE [DONE]. Breaking here would
// skip the usage chunk and zero out the caller's token totals. The
// [DONE] marker check above is the right termination signal.
} catch (error: unknown) {
console.warn('RequestyProvider: Failed to parse stream chunk JSON, skipping chunk. Data:', jsonData, 'Error:', error);
}
}
}
abortSignal?.removeEventListener('abort', abortHandler);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Streaming errors escape generator 🐞 Bug ☼ Reliability

RequestyProvider.generateCompletionStream() can throw if makeApiRequest() fails or parseSseStream()
errors, instead of emitting a final chunk with isFinal:true and an error payload, breaking the
documented streaming semantics consumers rely on.
Agent Prompt
### Issue description
`generateCompletionStream()` does not wrap request setup and SSE iteration in a try/catch. If `makeApiRequest()` throws (network/auth/etc) or `parseSseStream()` throws while reading, the async generator itself throws and no terminal `ModelCompletionResponse` with `isFinal: true` is emitted.

### Issue Context
The provider contract explicitly states streamed calls MUST emit a terminal chunk with `isFinal: true` even on error, so downstream consumers can always teardown deterministically.

### Fix Focus Areas
- src/core/llm/providers/IProvider.ts[19-29]
- src/core/llm/providers/implementations/RequestyProvider.ts[336-384]
- src/core/llm/providers/implementations/RequestyProvider.ts[711-742]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 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/core/config/AgentOSConfig.ts`:
- Around line 236-248: The Requesty provider configuration block is using
incorrect field names that do not match what RequestyProviderConfig expects. In
the config object within the Requesty provider block, rename the field
`defaultModel` to `defaultModelId` and rename the field `timeout` to
`requestTimeout` to ensure these configuration values are properly read by the
RequestyProvider instead of being silently ignored.
- Line 52: The new REQUESTY_API_KEY field has been added to the AgentOSConfig
interface, but the validation logic that checks for "no LLM provider configured"
warning does not include this new key in its condition. Find the warning
condition that validates whether at least one LLM provider is configured (likely
checking other API keys like OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) and add
REQUESTY_API_KEY to that check so that a Requesty-only deployment does not
trigger the warning.

In `@src/core/llm/providers/implementations/RequestyProvider.ts`:
- Around line 701-707: Remove the requestBodyPreview property from the error
object passed to RequestyProviderError in the throw statement, as it exposes
sensitive user data like prompts and tool arguments. Keep only the safe
properties like requestEndpoint, responseData, and underlyingError in the error
context.
- Around line 401-412: The issue is that customModelParams is spread directly
onto the root payload object, but the code then attempts to write inputType to
payload.customModelParams, creating a new nested structure that won't be sent in
the request. Instead of conditionally assigning to payload.customModelParams,
directly assign the input_type property to the root payload object when
options?.inputType exists, similar to how encoding_format and dimensions are
already being handled via the spread operator pattern.
- Around line 336-384: The abort signal handling in the stream method has two
issues: first, the abort check happens after makeApiRequest is already called,
so already-aborted signals don't prevent the API request, and second, the
removeEventListener call is unprotected and won't execute if the stream loop
throws an error. Move the abort signal check to occur before calling
makeApiRequest on the stream so that aborted calls don't hit the API, and wrap
both the stream loop and the removeEventListener cleanup in a try-finally block
to ensure the listener is always removed even if an error occurs during
streaming.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c55b6e7-f753-4058-a0e1-7ccdefee7b57

📥 Commits

Reviewing files that changed from the base of the PR and between 8f62962 and 1817c2c.

📒 Files selected for processing (5)
  • src/core/config/AgentOSConfig.ts
  • src/core/llm/providers/AIModelProviderManager.ts
  • src/core/llm/providers/errors/RequestyProviderError.ts
  • src/core/llm/providers/implementations/RequestyProvider.ts
  • src/core/llm/providers/structuredOutputFormat.ts

OPENAI_API_KEY?: string;
ANTHROPIC_API_KEY?: string;
OPENROUTER_API_KEY?: string;
REQUESTY_API_KEY?: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include Requesty in the “no LLM provider configured” warning condition.

After adding REQUESTY_API_KEY, a Requesty-only deployment still triggers the warning path because the condition doesn’t include this key.

💡 Suggested update
   if (
     !env.OPENAI_API_KEY &&
     !env.ANTHROPIC_API_KEY &&
     !env.OPENROUTER_API_KEY &&
+    !env.REQUESTY_API_KEY &&
     !env.OLLAMA_BASE_URL
   ) {
🤖 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/core/config/AgentOSConfig.ts` at line 52, The new REQUESTY_API_KEY field
has been added to the AgentOSConfig interface, but the validation logic that
checks for "no LLM provider configured" warning does not include this new key in
its condition. Find the warning condition that validates whether at least one
LLM provider is configured (likely checking other API keys like OPENAI_API_KEY,
ANTHROPIC_API_KEY, etc.) and add REQUESTY_API_KEY to that check so that a
Requesty-only deployment does not trigger the warning.

Comment on lines +236 to +248
// Requesty Provider (OpenAI-compatible LLM gateway)
if (env.REQUESTY_API_KEY) {
providers.push({
providerId: 'requesty',
enabled: true,
isDefault: !env.OPENAI_API_KEY && !env.OPENROUTER_API_KEY, // Default to Requesty if OpenAI/OpenRouter not available
config: {
apiKey: env.REQUESTY_API_KEY,
baseURL: 'https://router.requesty.ai/v1',
defaultModel: 'openai/gpt-4o',
maxRetries: 3,
timeout: 60000,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use RequestyProviderConfig field names in the Requesty block.

The config uses defaultModel and timeout, but RequestyProvider reads defaultModelId and requestTimeout. As-is, the intended defaults are silently ignored.

💡 Proposed fix
       config: {
         apiKey: env.REQUESTY_API_KEY,
         baseURL: 'https://router.requesty.ai/v1',
-        defaultModel: 'openai/gpt-4o',
-        maxRetries: 3,
-        timeout: 60000,
+        defaultModelId: 'openai/gpt-4o',
+        requestTimeout: 60000,
+        streamRequestTimeout: 180000,
       },
📝 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.

Suggested change
// Requesty Provider (OpenAI-compatible LLM gateway)
if (env.REQUESTY_API_KEY) {
providers.push({
providerId: 'requesty',
enabled: true,
isDefault: !env.OPENAI_API_KEY && !env.OPENROUTER_API_KEY, // Default to Requesty if OpenAI/OpenRouter not available
config: {
apiKey: env.REQUESTY_API_KEY,
baseURL: 'https://router.requesty.ai/v1',
defaultModel: 'openai/gpt-4o',
maxRetries: 3,
timeout: 60000,
},
// Requesty Provider (OpenAI-compatible LLM gateway)
if (env.REQUESTY_API_KEY) {
providers.push({
providerId: 'requesty',
enabled: true,
isDefault: !env.OPENAI_API_KEY && !env.OPENROUTER_API_KEY, // Default to Requesty if OpenAI/OpenRouter not available
config: {
apiKey: env.REQUESTY_API_KEY,
baseURL: 'https://router.requesty.ai/v1',
defaultModelId: 'openai/gpt-4o',
requestTimeout: 60000,
streamRequestTimeout: 180000,
},
🤖 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/core/config/AgentOSConfig.ts` around lines 236 - 248, The Requesty
provider configuration block is using incorrect field names that do not match
what RequestyProviderConfig expects. In the config object within the Requesty
provider block, rename the field `defaultModel` to `defaultModelId` and rename
the field `timeout` to `requestTimeout` to ensure these configuration values are
properly read by the RequestyProvider instead of being silently ignored.

Comment on lines +336 to +384
const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
'/chat/completions',
'POST',
// CR8: honor a per-call requestTimeout override over the stream default.
options.requestTimeout ?? this.config.streamRequestTimeout,
payload,
true
);

const accumulatedToolCalls: Map<number, { id?: string; type?: 'function'; function?: { name?: string; arguments?: string; } }> = new Map();

const abortSignal = options.abortSignal;
if (abortSignal?.aborted) {
yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted prior to first chunk', type: 'abort' }, isFinal: true };
return;
}
const abortHandler = () => { /* passive; loop logic handles emission */ };
abortSignal?.addEventListener('abort', abortHandler, { once: true });

for await (const rawChunk of this.parseSseStream(stream)) {
if (abortSignal?.aborted) {
yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted by caller', type: 'abort' }, isFinal: true };
break;
}
if (rawChunk.startsWith('data: ') && rawChunk.includes('[DONE]')) {
const doneData = rawChunk.substring('data: '.length).trim();
if (doneData === '[DONE]') break;
}
if (rawChunk === 'data: [DONE]') {
break;
}

if (rawChunk.startsWith('data: ')) {
const jsonData = rawChunk.substring('data: '.length);
try {
const apiChunk = JSON.parse(jsonData) as RequestyChatCompletionAPIResponse;
yield this.mapApiToStreamChunkResponse(apiChunk, modelId, accumulatedToolCalls);
// Don't break on finish_reason: with stream_options.include_usage,
// Requesty (like OpenAI) emits a trailing usage-only chunk AFTER
// the finish_reason chunk and BEFORE [DONE]. Breaking here would
// skip the usage chunk and zero out the caller's token totals. The
// [DONE] marker check above is the right termination signal.
} catch (error: unknown) {
console.warn('RequestyProvider: Failed to parse stream chunk JSON, skipping chunk. Data:', jsonData, 'Error:', error);
}
}
}
abortSignal?.removeEventListener('abort', abortHandler);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Respect abort before opening the stream and always clean up listeners.

abortSignal is checked only after the streaming request is created, so already-aborted calls still hit /chat/completions. Also, listener removal is not protected if the stream loop throws.

💡 Proposed fix
+    const abortSignal = options.abortSignal;
+    if (abortSignal?.aborted) {
+      yield {
+        id: `requesty-abort-${Date.now()}`,
+        object: 'chat.completion.chunk',
+        created: Math.floor(Date.now() / 1000),
+        modelId,
+        choices: [],
+        error: { message: 'Stream aborted prior to request dispatch', type: 'abort' },
+        isFinal: true
+      };
+      return;
+    }
+
     const stream = await this.makeApiRequest<NodeJS.ReadableStream>(
       '/chat/completions',
       'POST',
       options.requestTimeout ?? this.config.streamRequestTimeout,
       payload,
       true
     );
@@
-    const abortSignal = options.abortSignal;
-    if (abortSignal?.aborted) {
-      yield { id: `requesty-abort-${Date.now()}`, object: 'chat.completion.chunk', created: Math.floor(Date.now()/1000), modelId, choices: [], error: { message: 'Stream aborted prior to first chunk', type: 'abort' }, isFinal: true };
-      return;
-    }
     const abortHandler = () => { /* passive; loop logic handles emission */ };
     abortSignal?.addEventListener('abort', abortHandler, { once: true });
-
-    for await (const rawChunk of this.parseSseStream(stream)) {
+    try {
+      for await (const rawChunk of this.parseSseStream(stream)) {
         ...
-    }
-    abortSignal?.removeEventListener('abort', abortHandler);
+      }
+    } finally {
+      abortSignal?.removeEventListener('abort', abortHandler);
+    }
🤖 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/core/llm/providers/implementations/RequestyProvider.ts` around lines 336
- 384, The abort signal handling in the stream method has two issues: first, the
abort check happens after makeApiRequest is already called, so already-aborted
signals don't prevent the API request, and second, the removeEventListener call
is unprotected and won't execute if the stream loop throws an error. Move the
abort signal check to occur before calling makeApiRequest on the stream so that
aborted calls don't hit the API, and wrap both the stream loop and the
removeEventListener cleanup in a try-finally block to ensure the listener is
always removed even if an error occurs during streaming.

Comment on lines +401 to +412
const payload: Record<string, unknown> = {
model: modelId,
input: texts,
...(options?.encodingFormat && { encoding_format: options.encodingFormat }),
...(options?.dimensions && { dimensions: options.dimensions }),
...(options?.customModelParams || {}),
};
if (options?.inputType && payload.customModelParams && typeof payload.customModelParams === 'object') {
(payload.customModelParams as Record<string, unknown>).input_type = options.inputType;
} else if (options?.inputType) {
payload.customModelParams = { input_type: options.inputType };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

inputType is written to the wrong payload shape.

customModelParams is spread onto the root payload, but inputType is later written under payload.customModelParams. That nested key is not part of the current request shape and can drop the parameter.

💡 Proposed fix
     const payload: Record<string, unknown> = {
       model: modelId,
       input: texts,
       ...(options?.encodingFormat && { encoding_format: options.encodingFormat }),
       ...(options?.dimensions && { dimensions: options.dimensions }),
+      ...(options?.inputType && { input_type: options.inputType }),
       ...(options?.customModelParams || {}),
     };
-    if (options?.inputType && payload.customModelParams && typeof payload.customModelParams === 'object') {
-      (payload.customModelParams as Record<string, unknown>).input_type = options.inputType;
-    } else if (options?.inputType) {
-      payload.customModelParams = { input_type: options.inputType };
-    }
🤖 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/core/llm/providers/implementations/RequestyProvider.ts` around lines 401
- 412, The issue is that customModelParams is spread directly onto the root
payload object, but the code then attempts to write inputType to
payload.customModelParams, creating a new nested structure that won't be sent in
the request. Instead of conditionally assigning to payload.customModelParams,
directly assign the input_type property to the root payload object when
options?.inputType exists, similar to how encoding_format and dimensions are
already being handled via the spread operator pattern.

Comment on lines +701 to +707
throw new RequestyProviderError(
decoratedMessage,
'API_REQUEST_FAILED',
statusCode,
errorType,
{ requestEndpoint: endpoint, requestBodyPreview: body ? JSON.stringify(body).substring(0, 200) + '...' : undefined, responseData: errorData, underlyingError: error }
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not attach raw request-body previews to provider errors.

requestBodyPreview can include user prompts/tool args and leaks sensitive data when error details are logged upstream.

💡 Proposed fix
       throw new RequestyProviderError(
         decoratedMessage,
         'API_REQUEST_FAILED',
         statusCode,
         errorType,
-        { requestEndpoint: endpoint, requestBodyPreview: body ? JSON.stringify(body).substring(0, 200) + '...' : undefined, responseData: errorData, underlyingError: error }
+        {
+          requestEndpoint: endpoint,
+          requestBodyKeys: body ? Object.keys(body) : undefined,
+          responseData: errorData,
+          underlyingError: error
+        }
       );
📝 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.

Suggested change
throw new RequestyProviderError(
decoratedMessage,
'API_REQUEST_FAILED',
statusCode,
errorType,
{ requestEndpoint: endpoint, requestBodyPreview: body ? JSON.stringify(body).substring(0, 200) + '...' : undefined, responseData: errorData, underlyingError: error }
);
throw new RequestyProviderError(
decoratedMessage,
'API_REQUEST_FAILED',
statusCode,
errorType,
{
requestEndpoint: endpoint,
requestBodyKeys: body ? Object.keys(body) : undefined,
responseData: errorData,
underlyingError: error
}
);
🤖 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/core/llm/providers/implementations/RequestyProvider.ts` around lines 701
- 707, Remove the requestBodyPreview property from the error object passed to
RequestyProviderError in the throw statement, as it exposes sensitive user data
like prompts and tool arguments. Keep only the safe properties like
requestEndpoint, responseData, and underlyingError in the error context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant