Skip to content

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

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

feat: add Requesty as an OpenAI-compatible LLM provider#17
Thibaultjaigu wants to merge 3 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 (optionally REQUESTY_BASE_URL), with support for chat completions, streaming, embeddings, and model discovery.
    • Extended provider auto-detection so Requesty is selected when other provider keys are not set.
  • Bug Fixes
    • Improved streamed response role selection to correctly prefer the provided delta role.
  • Tests
    • Updated provider defaults and auto-detection coverage to include Requesty.

@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

📝 Walkthrough

Walkthrough

Adds Requesty provider support across configuration, runtime defaults, provider registration, structured output handling, and the provider implementation itself. Also corrects OpenRouter streamed role selection.

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 Requesty response metadata. RequestyProviderConfig and internal Requesty request/response, streaming, embedding, and model metadata types are defined.
Provider initialization, model cache, and message mapping
src/core/llm/providers/implementations/RequestyProvider.ts
RequestyProvider.initialize() validates the API key, creates the Axios client, and refreshes the model cache. refreshAvailableModels() maps Requesty model data into internal model metadata. mapToRequestyMessages() converts internal chat messages into Requesty payloads.
Completion, streaming, embeddings, and model management
src/core/llm/providers/implementations/RequestyProvider.ts
generateCompletion() posts to /chat/completions. generateCompletionStream() consumes SSE chunks and emits incremental updates. generateEmbeddings() posts to /embeddings. listAvailableModels(), getModelInfo(), checkHealth(), and shutdown() complete the provider API.
Response mappers and HTTP helpers
src/core/llm/providers/implementations/RequestyProvider.ts
mapApiToCompletionResponse() maps non-stream responses into ModelCompletionResponse. mapApiToStreamChunkResponse() reconstructs streamed tool-call deltas and final chunks. makeApiRequest() normalizes Axios failures into RequestyProviderError. parseSseStream() yields SSE lines from readable streams.
Provider registration, config factory, runtime defaults, and structured output routing
src/core/llm/providers/AIModelProviderManager.ts, src/core/config/AgentOSConfig.ts, src/api/model.ts, src/api/runtime/provider-defaults.ts, src/api/runtime/__tests__/provider-defaults.test.ts, src/core/llm/providers/structuredOutputFormat.ts
AIModelProviderManager imports RequestyProvider, widens the provider config union, and adds a requesty initialization branch. EnvironmentConfig reads REQUESTY_API_KEY, createModelProviderManagerConfig() registers Requesty conditionally, src/api/model.ts resolves Requesty env variables, provider-defaults.ts adds Requesty defaults and auto-detection, and tests cover the new runtime detection path. buildResponseFormat routes requesty to best-effort JSON output.

OpenRouter Streaming Role Fix

Layer / File(s) Summary
Streaming final message role selection
src/core/llm/providers/implementations/OpenRouterProvider.ts
The streamed final-message role selection in mapApiToStreamChunkResponse() now prefers choice.delta?.role when it is defined.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through keys and models new,
Requesty streams came shining through.
The rabbit twitched an ear and smiled,
With roles now kept and routes compiled.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding Requesty as an OpenAI-compatible LLM provider.
Description check ✅ Passed The description covers what changed and why, but it does not follow the template's checklist format or include a related issue reference.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

qodo-free-for-open-source-projects Bot commented Jun 23, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Requesty not env-resolved ✓ 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 ✓ Resolved 🐞 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 ✓ Resolved 🐞 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 thread src/core/llm/providers/AIModelProviderManager.ts
Comment thread src/core/config/AgentOSConfig.ts
Comment thread src/core/llm/providers/implementations/RequestyProvider.ts Outdated

@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

Comment thread src/core/config/AgentOSConfig.ts
Comment thread src/core/config/AgentOSConfig.ts
Comment thread src/core/llm/providers/implementations/RequestyProvider.ts Outdated
Comment thread src/core/llm/providers/implementations/RequestyProvider.ts Outdated
Comment thread src/core/llm/providers/implementations/RequestyProvider.ts
…m error contract, abort, embeddings payload)
@Thibaultjaigu

Copy link
Copy Markdown
Author

Thanks for the thorough reviews — pushed fixes in d431e8d:

  • Config key mismatch (CodeRabbit/Qodo): AgentOSConfig.ts now passes defaultModelId / requestTimeout / streamRequestTimeout, matching RequestyProviderConfig (was defaultModel / timeout / maxRetries, which were silently ignored). Dropped maxRetries (not in the interface).
  • Requesty-only "no LLM provider" warning (CodeRabbit): added !env.REQUESTY_API_KEY to the warning condition so a Requesty-only deployment no longer warns.
  • Streaming error contract (Qodo): wrapped request setup + SSE iteration in generateCompletionStream() in try/catch; on failure it now yields a terminal chunk with isFinal: true and an error payload instead of throwing, per the IProvider contract.
  • Abort before stream + listener cleanup (CodeRabbit/Sourcery): the abortSignal?.aborted check now happens before the request is dispatched, and removeEventListener runs in a finally.
  • Embeddings inputType (CodeRabbit): now written as a root input_type field instead of a nested customModelParams key.
  • High-level API resolution (Qodo): added requesty to ENV_KEY_MAP/ENV_URL_MAP in src/api/model.ts and a requesty entry to PROVIDER_DEFAULTS + AUTO_DETECT_ORDER in provider-defaults.ts, with a test case, so REQUESTY_API_KEY env auto-detect and provider: 'requesty' resolution work end-to-end.
  • Error detail leak (CodeRabbit): swapped requestBodyPreview for requestBodyKeys so prompt/tool content isn't attached to errors.

Skipped the Sourcery "introduce helper types / reduce complexity" refactor to keep the diff minimal and the role-precedence nit (already resolved in eb55cf4).

tsc --noEmit is green and it's tested live against https://router.requesty.ai/v1/chat/completions (HTTP 200, openai/gpt-4o-mini).

@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.

🧹 Nitpick comments (1)
src/api/runtime/provider-defaults.ts (1)

58-61: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

requesty defaults omit an embedding model.

Requesty exposes an OpenAI-compatible /embeddings endpoint and the new RequestyProvider implements generateEmbeddings(), but PROVIDER_DEFAULTS.requesty only declares text and cheap. autoDetectProvider('embedding') skips any provider lacking the requested task (Line 191), so Requesty will never be auto-selected for embeddings even when REQUESTY_API_KEY is the only key set. Consider adding an embedding default if that path is expected to work.

🤖 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/api/runtime/provider-defaults.ts` around lines 58 - 61, The Requesty
provider defaults are missing an embedding model, so auto-detection cannot
select Requesty for embedding tasks. Update PROVIDER_DEFAULTS.requesty in
provider-defaults to include an embedding entry that matches the model supported
by RequestyProvider.generateEmbeddings(), so autoDetectProvider('embedding') can
consider Requesty when only REQUESTY_API_KEY is available.
🤖 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.

Nitpick comments:
In `@src/api/runtime/provider-defaults.ts`:
- Around line 58-61: The Requesty provider defaults are missing an embedding
model, so auto-detection cannot select Requesty for embedding tasks. Update
PROVIDER_DEFAULTS.requesty in provider-defaults to include an embedding entry
that matches the model supported by RequestyProvider.generateEmbeddings(), so
autoDetectProvider('embedding') can consider Requesty when only REQUESTY_API_KEY
is available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e70acee-13ac-4c19-a8e0-e323d277bd19

📥 Commits

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

📒 Files selected for processing (5)
  • src/api/model.ts
  • src/api/runtime/__tests__/provider-defaults.test.ts
  • src/api/runtime/provider-defaults.ts
  • src/core/config/AgentOSConfig.ts
  • src/core/llm/providers/implementations/RequestyProvider.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/config/AgentOSConfig.ts
  • src/core/llm/providers/implementations/RequestyProvider.ts

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