Skip to content

feat: add Requesty as an OpenAI-compatible provider - #63

Open
Thibaultjaigu wants to merge 2 commits into
teilomillet:mainfrom
Thibaultjaigu:add-requesty-provider
Open

feat: add Requesty as an OpenAI-compatible provider#63
Thibaultjaigu wants to merge 2 commits into
teilomillet:mainfrom
Thibaultjaigu:add-requesty-provider

Conversation

@Thibaultjaigu

@Thibaultjaigu Thibaultjaigu commented Jun 30, 2026

Copy link
Copy Markdown

This adds a dedicated requesty provider, mirroring the existing OpenRouter provider as closely as possible.

Requesty is an OpenAI-compatible LLM gateway that exposes many models through a single API using provider/model naming (for example openai/gpt-4o, anthropic/claude-sonnet-4-5), the same convention as OpenRouter, so the provider mirrors the OpenRouter implementation.

Changes:

  • providers/requesty.go: new provider implementing the Provider interface, modeled on openrouter.go. Uses base URL https://router.requesty.ai/v1, Bearer auth, and the OpenAI-compatible chat/completions, streaming, tool-calling and JSON-schema paths. The OpenRouter-only openrouter/auto auto-route sentinel was dropped since it is not applicable; fallback_models and provider routing preferences are kept.
  • providers/provider.go: register requesty in the constructor map and the standard provider config map (TypeOpenAI endpoint).
  • README.md: add Requesty to the providers list.

Verification: I tested this against the live endpoint before opening the PR. Building a requesty provider through the registry and calling the API returned HTTP 200, and ParseResponse returned the model output. go build ./..., go vet ./providers/, and go test ./providers/ all pass.

Docs: https://requesty.ai , https://docs.requesty.ai , https://app.requesty.ai/api-keys

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

Summary by Sourcery

Add a new Requesty LLM provider and register it as an OpenAI-compatible backend.

New Features:

  • Introduce a Requesty provider implementing the Provider interface with chat, legacy completions, streaming, tool-calling, and JSON-schema support.
  • Register Requesty as an OpenAI-compatible provider in the provider registry with appropriate endpoint and authentication configuration.

Documentation:

  • Document Requesty as a supported provider in the README providers list.

@sourcery-ai

sourcery-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a new OpenAI-compatible Requesty provider modeled on the existing OpenRouter provider, wiring it into the provider registry/config and updating docs.

Sequence diagram for using RequestyProvider via ProviderRegistry

sequenceDiagram
    participant App
    participant ProviderRegistry
    participant RequestyProvider
    participant RequestyAPI

    App->>ProviderRegistry: NewProviderRegistry("requesty")
    ProviderRegistry-->>App: *ProviderRegistry

    App->>ProviderRegistry: (create provider) NewRequestyProvider(apiKey, model, extraHeaders)
    ProviderRegistry-->>App: RequestyProvider

    App->>RequestyProvider: SetDefaultOptions(config)
    App->>RequestyProvider: PrepareRequest(prompt, options)
    RequestyProvider-->>App: requestBody

    App->>RequestyAPI: POST Endpoint() with Headers() and requestBody
    RequestyAPI-->>App: responseBody

    App->>RequestyProvider: ParseResponse(responseBody)
    RequestyProvider-->>App: completionText
Loading

File-Level Changes

Change Details Files
Introduce RequestyProvider implementing the Provider interface with OpenAI-compatible chat, completions, streaming, tool-calling, and JSON-schema support, modeled on OpenRouter.
  • Define RequestyProvider struct with API key, model, options, extraHeaders, and logger fields and constructor NewRequestyProvider.
  • Implement Provider interface methods for name, endpoints (chat, legacy completions, generation), headers, option management, JSON-schema support, and extra headers.
  • Implement request preparation for prompts, completions, messages, and JSON schema, including fallback model routing and provider preferences.
  • Support multimodal input via shared helper functions for images and message content normalization.
  • Implement response parsing for chat and legacy text completions, including error handling, logging generation IDs/model routing, and tool/function-call handling.
  • Add streaming support including request preparation and incremental chunk parsing, with logging of token usage and tool calls.
providers/requesty.go
Register Requesty as a standard OpenAI-type provider in the provider registry and config map.
  • Add NewRequestyProvider to the provider constructor registry map under key "requesty".
  • Add a Requesty ProviderConfig entry using TypeOpenAI with router.requesty.ai chat/completions endpoint, Bearer auth, JSON content type, JSON schema and streaming support flags.
providers/provider.go
Update documentation to list Requesty as a supported provider.
  • Mention Requesty alongside existing providers in the "Unified API for Multiple LLM Providers" feature description.
README.md

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
Contributor

Choose a reason for hiding this comment

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

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

  • There’s a fair amount of duplication between PrepareRequest, PrepareCompletionRequest, PrepareRequestWithSchema, and PrepareRequestWithMessages (e.g., model/options/fallback_models/provider_preferences handling); consider extracting a shared helper to reduce divergence and make future changes less error-prone.
  • Several places assume concrete types for options (e.g., fallback_models as []string, provider_preferences as map[string]interface{}, images as []types.ContentPart); if callers pass these via generic configuration maps you may want to normalize/convert from []interface{}/other shapes to avoid panics or silent no-ops.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There’s a fair amount of duplication between `PrepareRequest`, `PrepareCompletionRequest`, `PrepareRequestWithSchema`, and `PrepareRequestWithMessages` (e.g., model/options/fallback_models/provider_preferences handling); consider extracting a shared helper to reduce divergence and make future changes less error-prone.
- Several places assume concrete types for options (e.g., `fallback_models` as `[]string`, `provider_preferences` as `map[string]interface{}`, `images` as `[]types.ContentPart`); if callers pass these via generic configuration maps you may want to normalize/convert from `[]interface{}`/other shapes to avoid panics or silent no-ops.

## Individual Comments

### Comment 1
<location path="providers/requesty.go" line_range="71-72" />
<code_context>
+
+// GenerationEndpoint returns the Requesty API endpoint for retrieving generation details.
+// This can be used to query stats like cost and token usage after a request.
+func (p *RequestyProvider) GenerationEndpoint(generationID string) string {
+	return fmt.Sprintf("https://router.requesty.ai/v1/generation?id=%s", generationID)
+}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** URL encode the generation ID when building the GenerationEndpoint URL

`generationID` is inserted into the query string without encoding, so characters like `+`, `?`, `&`, or spaces could produce an invalid or ambiguous URL. Please apply `url.QueryEscape` to `generationID` (and import `net/url`) before formatting the URL to ensure it’s always safe.
</issue_to_address>

### Comment 2
<location path="providers/requesty.go" line_range="483-484" />
<code_context>
+
+// ParseStreamResponse processes a chunk from a streaming Requesty response.
+func (p *RequestyProvider) ParseStreamResponse(chunk []byte) (string, error) {
+	// Skip empty chunks and "[DONE]" markers
+	if len(chunk) == 0 || string(chunk) == "[DONE]" {
+		return "", nil
+	}
</code_context>
<issue_to_address>
**suggestion:** Handle common streaming framing (e.g., prefixed "data:" and whitespace) when skipping DONE markers

This currently only skips chunks equal to `"[DONE]"`. In many SSE implementations, lines look like `"data: [DONE]\n\n"`, with a `data:` prefix and trailing whitespace. If that framing isn’t stripped earlier, `ParseStreamResponse` will try to JSON-decode these control lines and fail. Please trim whitespace and optionally remove a leading `"data:"` before comparing against the `[DONE]` sentinel.

Suggested implementation:

```golang
// ParseStreamResponse processes a chunk from a streaming Requesty response.
func (p *RequestyProvider) ParseStreamResponse(chunk []byte) (string, error) {
	// Treat empty chunks as no-op
	if len(chunk) == 0 {
		return "", nil
	}

	// Normalize common SSE framing: trim whitespace and optional "data:" prefix
	line := strings.TrimSpace(string(chunk))
	if line == "" {
		return "", nil
	}
	if strings.HasPrefix(line, "data:") {
		line = strings.TrimSpace(line[len("data:"):])
	}

	// Skip "[DONE]" control messages
	if line == "[DONE]" {
		return "", nil
	}

	// NOTE: If higher-level code expects parsed JSON or a specific payload shape,
	// this is where that parsing should occur. For now, we return the normalized line.
	return line, nil
}

```

1. Ensure `providers/requesty.go` imports the `strings` package. For example, in the import block add:
   `import "strings"` (or include it in the existing grouped import).
2. If other parts of the code expect `ParseStreamResponse` to JSON-decode the chunk into a specific structure, move that JSON parsing to happen after the `[DONE]` filtering using `line` rather than the raw `chunk`.
</issue_to_address>

### Comment 3
<location path="providers/requesty.go" line_range="107-113" />
<code_context>
+
+// Headers returns the HTTP headers required for Requesty API requests.
+func (p *RequestyProvider) Headers() map[string]string {
+	headers := map[string]string{
+		"Content-Type":  "application/json",
+		"Authorization": "Bearer " + p.apiKey,
+		"HTTP-Referer":  "https://github.com/teilomillet/gollm", // Identify the app to Requesty
+	}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Use a standard Referer header name instead of HTTP-Referer unless the API requires it

`HTTP-Referer` is non-standard; the usual header is `Referer`. Unless Requesty’s docs specify `HTTP-Referer`, prefer `Referer` (or send both) for better interoperability with typical HTTP tooling and providers.

```suggestion
 // Headers returns the HTTP headers required for Requesty API requests.
func (p *RequestyProvider) Headers() map[string]string {
	headers := map[string]string{
		"Content-Type":  "application/json",
		"Authorization": "Bearer " + p.apiKey,
		// Use the standard Referer header for interoperability, and also send HTTP-Referer
		// in case the Requesty API expects that non-standard header.
		"Referer":       "https://github.com/teilomillet/gollm", // Identify the app to Requesty
		"HTTP-Referer":  "https://github.com/teilomillet/gollm",
	}
```
</issue_to_address>

### Comment 4
<location path="providers/requesty.go" line_range="125" />
<code_context>
+	return headers
+}
+
+// PrepareRequest creates a chat completion request for the Requesty API.
+func (p *RequestyProvider) PrepareRequest(prompt string, options map[string]interface{}) ([]byte, error) {
+	// Start with the passed options
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared request-building, message-construction, and response-struct logic into private helpers to eliminate duplication and keep each public method focused on its specific behavior.

You can significantly cut duplication and tighten behavior by centralizing the common request/response plumbing into small helpers without changing functionality.

### 1. Factor out common request-building

`PrepareRequest`, `PrepareCompletionRequest`, `PrepareRequestWithSchema`, and `PrepareRequestWithMessages` all manually:

- copy `options`
- set `model`
- merge `p.options`
- handle `fallback_models``models`
- handle `provider_preferences``provider`
- sometimes handle `stream`, `tools`, `tool_choice`, `enable_prompt_caching`

This can be centralized in a private helper and reused.

```go
// inside RequestyProvider

func (p *RequestyProvider) baseRequest(options map[string]interface{}) map[string]interface{} {
	req := make(map[string]interface{}, len(options)+len(p.options)+4)
	for k, v := range options {
		req[k] = v
	}

	// model
	req["model"] = p.model

	// merge provider options (do not override call-site options)
	for k, v := range p.options {
		if _, exists := req[k]; !exists {
			req[k] = v
		}
	}

	// fallback_models -> models
	if fallbackModels, ok := req["fallback_models"].([]string); ok {
		req["models"] = append([]string{p.model}, fallbackModels...)
		delete(req, "fallback_models")
	}

	// provider_preferences -> provider
	if providerPrefs, ok := req["provider_preferences"].(map[string]interface{}); ok {
		req["provider"] = providerPrefs
		delete(req, "provider_preferences")
	}

	return req
}
```

Then each public method only focuses on its specifics:

```go
func (p *RequestyProvider) PrepareRequest(prompt string, options map[string]interface{}) ([]byte, error) {
	req := p.baseRequest(options)

	messages := p.buildMessagesForPrompt(prompt, req)
	req["messages"] = messages

	// tools / tool_choice
	if tools, ok := req["tools"].([]interface{}); ok && len(tools) > 0 {
		req["tools"] = tools
	}
	if toolChoice, ok := req["tool_choice"]; ok {
		req["tool_choice"] = toolChoice
	}

	// stream
	if stream, ok := req["stream"].(bool); ok && stream {
		req["stream"] = true
	}

	// prompt caching flag is purely advisory for Requesty
	if caching, ok := req["enable_prompt_caching"].(bool); ok && caching {
		delete(req, "enable_prompt_caching")
	}

	return json.Marshal(req)
}
```

```go
func (p *RequestyProvider) PrepareCompletionRequest(prompt string, options map[string]interface{}) ([]byte, error) {
	req := p.baseRequest(options)

	req["prompt"] = prompt

	if stream, ok := req["stream"].(bool); ok && stream {
		req["stream"] = true
	}

	return json.Marshal(req)
}
```

```go
func (p *RequestyProvider) PrepareRequestWithSchema(prompt string, options map[string]interface{}, schema interface{}) ([]byte, error) {
	optsCopy := make(map[string]interface{}, len(options)+1)
	for k, v := range options {
		optsCopy[k] = v
	}
	optsCopy["response_format"] = map[string]interface{}{
		"type":   "json_object",
		"schema": schema,
	}

	// reuse normal chat request path
	return p.PrepareRequest(prompt, optsCopy)
}
```

```go
func (p *RequestyProvider) PrepareRequestWithMessages(messages []types.MemoryMessage, options map[string]interface{}) ([]byte, error) {
	req := p.baseRequest(options)

	req["messages"] = p.buildMessagesFromMemory(messages, req)

	// tools / tool_choice, stream as above
	if tools, ok := req["tools"].([]interface{}); ok && len(tools) > 0 {
		req["tools"] = tools
	}
	if toolChoice, ok := req["tool_choice"]; ok {
		req["tool_choice"] = toolChoice
	}
	if stream, ok := req["stream"].(bool); ok && stream {
		req["stream"] = true
	}

	// prompt caching handled in buildMessagesFromMemory; drop flag
	delete(req, "enable_prompt_caching")

	return json.Marshal(req)
}
```

`PrepareStreamRequest` can also reuse `baseRequest` or just wrap `PrepareRequest` as you already do.

### 2. Factor message-building (single prompt vs memory)

You can reuse the same multimodal/image handling and system-message behavior between prompt-based and memory-based calls.

```go
func (p *RequestyProvider) buildMessagesForPrompt(prompt string, req map[string]interface{}) []map[string]interface{} {
	var messages []map[string]interface{}

	if sysMsg, ok := req["system_message"].(string); ok {
		messages = append(messages, map[string]interface{}{
			"role":    "system",
			"content": sysMsg,
		})
		delete(req, "system_message")
	}

	// multimodal user message
	if images, ok := req["images"].([]types.ContentPart); ok && len(images) > 0 {
		contentArray := []map[string]interface{}{
			{"type": "text", "text": prompt},
		}
		contentArray = append(contentArray, ConvertImagesToOpenAIContent(images)...)
		delete(req, "images")

		messages = append(messages, map[string]interface{}{
			"role":    "user",
			"content": contentArray,
		})
	} else {
		messages = append(messages, map[string]interface{}{
			"role":    "user",
			"content": prompt,
		})
	}

	return messages
}
```

```go
func (p *RequestyProvider) buildMessagesFromMemory(messages []types.MemoryMessage, req map[string]interface{}) []map[string]interface{} {
	formatted := make([]map[string]interface{}, 0, len(messages))
	for _, msg := range messages {
		formattedMsg := map[string]interface{}{"role": msg.Role}

		if msg.HasMultiContent() {
			formattedMsg["content"] = BuildOpenAIContentFromParts(msg.MultiContent)
		} else {
			formattedMsg["content"] = msg.Content
			formattedMsg["content"] = p.applyPromptCachingIfNeeded(msg, req, formattedMsg["content"])
		}

		formatted = append(formatted, formattedMsg)
	}

	// last user message + images (reusing existing helper)
	if images, ok := req["images"].([]types.ContentPart); ok && len(images) > 0 {
		for i := len(formatted) - 1; i >= 0; i-- {
			if formatted[i]["role"] == "user" {
				contentArray := NormalizeContentArray(formatted[i]["content"])
				contentArray = append(contentArray, ConvertImagesToOpenAIContent(images)...)
				formatted[i]["content"] = contentArray
				break
			}
		}
		delete(req, "images")
	}

	return formatted
}
```

And isolate the Anthropic prompt-caching mutation:

```go
func (p *RequestyProvider) applyPromptCachingIfNeeded(msg types.MemoryMessage, req map[string]interface{}, content interface{}) interface{} {
	caching, ok := req["enable_prompt_caching"].(bool)
	if !ok || !caching || msg.Role != "user" {
		return content
	}
	text, ok := msg.Content.(string)
	if !ok || len(text) <= 1000 {
		return content
	}
	if !strings.HasPrefix(p.model, "anthropic/") {
		return content
	}
	return []map[string]interface{}{
		{
			"type": "text",
			"text": text,
			"cache_control": map[string]string{
				"type": "ephemeral",
			},
		},
	}
}
```

This keeps the main message loops straightforward and moves provider-specific heuristics into a single, testable function.

### 3. Share response types between parsing functions

`ParseResponse`, `HandleFunctionCalls`, and `ParseStreamResponse` each define similar anonymous structs. These can be lifted to private types at the top of the file:

```go
type requestyChatResponse struct {
	Choices []struct {
		Message struct {
			Content      string           `json:"content"`
			FunctionCall *json.RawMessage `json:"function_call"`
			ToolCalls    []struct {
				ID       string `json:"id"`
				Type     string `json:"type"`
				Function struct {
					Name      string          `json:"name"`
					Arguments json.RawMessage `json:"arguments"`
				} `json:"function"`
			} `json:"tool_calls"`
		} `json:"message"`
		FinishReason       string `json:"finish_reason"`
		NativeFinishReason string `json:"native_finish_reason"`
	} `json:"choices"`
	Error struct {
		Message string `json:"message"`
	} `json:"error"`
	ID    string `json:"id"`
	Model string `json:"model"`
}

type requestyTextResponse struct {
	Choices []struct {
		Text string `json:"text"`
	} `json:"choices"`
	Error struct {
		Message string `json:"message"`
	} `json:"error"`
	ID    string `json:"id"`
	Model string `json:"model"`
}

type requestyStreamChunk struct {
	Choices []struct {
		Delta struct {
			Content   string `json:"content"`
			ToolCalls []struct {
				ID       string `json:"id"`
				Type     string `json:"type"`
				Function struct {
					Name      string          `json:"name"`
					Arguments json.RawMessage `json:"arguments"`
				} `json:"function"`
			} `json:"tool_calls"`
		} `json:"delta"`
		FinishReason string `json:"finish_reason"`
	} `json:"choices"`
	Error struct {
		Message string `json:"message"`
	} `json:"error"`
	ID    string `json:"id"`
	Model string `json:"model"`
	Usage *struct {
		PromptTokens     int `json:"prompt_tokens"`
		CompletionTokens int `json:"completion_tokens"`
		TotalTokens      int `json:"total_tokens"`
	} `json:"usage"`
}
```

Usage:

```go
func (p *RequestyProvider) ParseResponse(body []byte) (string, error) {
	var chatResp requestyChatResponse
	chatErr := json.Unmarshal(body, &chatResp)
	// ... unchanged logic ...
	var textResp requestyTextResponse
	// ...
}
```

```go
func (p *RequestyProvider) HandleFunctionCalls(body []byte) ([]byte, error) {
	var resp requestyChatResponse
	// ...
}
```

```go
func (p *RequestyProvider) ParseStreamResponse(chunk []byte) (string, error) {
	var resp requestyStreamChunk
	// ...
}
```

This reduces repetition and centralizes any future changes to the response shape.

All of these changes keep the existing behavior but substantially trim the file, reduce duplication, and make future modifications (especially around options handling and response formats) safer and more consistent.
</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 providers/requesty.go Outdated
Comment thread providers/requesty.go
Comment on lines +483 to +484
// Skip empty chunks and "[DONE]" markers
if len(chunk) == 0 || string(chunk) == "[DONE]" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Handle common streaming framing (e.g., prefixed "data:" and whitespace) when skipping DONE markers

This currently only skips chunks equal to "[DONE]". In many SSE implementations, lines look like "data: [DONE]\n\n", with a data: prefix and trailing whitespace. If that framing isn’t stripped earlier, ParseStreamResponse will try to JSON-decode these control lines and fail. Please trim whitespace and optionally remove a leading "data:" before comparing against the [DONE] sentinel.

Suggested implementation:

// ParseStreamResponse processes a chunk from a streaming Requesty response.
func (p *RequestyProvider) ParseStreamResponse(chunk []byte) (string, error) {
	// Treat empty chunks as no-op
	if len(chunk) == 0 {
		return "", nil
	}

	// Normalize common SSE framing: trim whitespace and optional "data:" prefix
	line := strings.TrimSpace(string(chunk))
	if line == "" {
		return "", nil
	}
	if strings.HasPrefix(line, "data:") {
		line = strings.TrimSpace(line[len("data:"):])
	}

	// Skip "[DONE]" control messages
	if line == "[DONE]" {
		return "", nil
	}

	// NOTE: If higher-level code expects parsed JSON or a specific payload shape,
	// this is where that parsing should occur. For now, we return the normalized line.
	return line, nil
}
  1. Ensure providers/requesty.go imports the strings package. For example, in the import block add:
    import "strings" (or include it in the existing grouped import).
  2. If other parts of the code expect ParseStreamResponse to JSON-decode the chunk into a specific structure, move that JSON parsing to happen after the [DONE] filtering using line rather than the raw chunk.

Comment thread providers/requesty.go
Comment on lines +107 to +113
// Headers returns the HTTP headers required for Requesty API requests.
func (p *RequestyProvider) Headers() map[string]string {
headers := map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer " + p.apiKey,
"HTTP-Referer": "https://github.com/teilomillet/gollm", // Identify the app to Requesty
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Use a standard Referer header name instead of HTTP-Referer unless the API requires it

HTTP-Referer is non-standard; the usual header is Referer. Unless Requesty’s docs specify HTTP-Referer, prefer Referer (or send both) for better interoperability with typical HTTP tooling and providers.

Suggested change
// Headers returns the HTTP headers required for Requesty API requests.
func (p *RequestyProvider) Headers() map[string]string {
headers := map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer " + p.apiKey,
"HTTP-Referer": "https://github.com/teilomillet/gollm", // Identify the app to Requesty
}
// Headers returns the HTTP headers required for Requesty API requests.
func (p *RequestyProvider) Headers() map[string]string {
headers := map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer " + p.apiKey,
// Use the standard Referer header for interoperability, and also send HTTP-Referer
// in case the Requesty API expects that non-standard header.
"Referer": "https://github.com/teilomillet/gollm", // Identify the app to Requesty
"HTTP-Referer": "https://github.com/teilomillet/gollm",
}

Comment thread providers/requesty.go
return headers
}

// PrepareRequest creates a chat completion request for the Requesty API.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider extracting shared request-building, message-construction, and response-struct logic into private helpers to eliminate duplication and keep each public method focused on its specific behavior.

You can significantly cut duplication and tighten behavior by centralizing the common request/response plumbing into small helpers without changing functionality.

1. Factor out common request-building

PrepareRequest, PrepareCompletionRequest, PrepareRequestWithSchema, and PrepareRequestWithMessages all manually:

  • copy options
  • set model
  • merge p.options
  • handle fallback_modelsmodels
  • handle provider_preferencesprovider
  • sometimes handle stream, tools, tool_choice, enable_prompt_caching

This can be centralized in a private helper and reused.

// inside RequestyProvider

func (p *RequestyProvider) baseRequest(options map[string]interface{}) map[string]interface{} {
	req := make(map[string]interface{}, len(options)+len(p.options)+4)
	for k, v := range options {
		req[k] = v
	}

	// model
	req["model"] = p.model

	// merge provider options (do not override call-site options)
	for k, v := range p.options {
		if _, exists := req[k]; !exists {
			req[k] = v
		}
	}

	// fallback_models -> models
	if fallbackModels, ok := req["fallback_models"].([]string); ok {
		req["models"] = append([]string{p.model}, fallbackModels...)
		delete(req, "fallback_models")
	}

	// provider_preferences -> provider
	if providerPrefs, ok := req["provider_preferences"].(map[string]interface{}); ok {
		req["provider"] = providerPrefs
		delete(req, "provider_preferences")
	}

	return req
}

Then each public method only focuses on its specifics:

func (p *RequestyProvider) PrepareRequest(prompt string, options map[string]interface{}) ([]byte, error) {
	req := p.baseRequest(options)

	messages := p.buildMessagesForPrompt(prompt, req)
	req["messages"] = messages

	// tools / tool_choice
	if tools, ok := req["tools"].([]interface{}); ok && len(tools) > 0 {
		req["tools"] = tools
	}
	if toolChoice, ok := req["tool_choice"]; ok {
		req["tool_choice"] = toolChoice
	}

	// stream
	if stream, ok := req["stream"].(bool); ok && stream {
		req["stream"] = true
	}

	// prompt caching flag is purely advisory for Requesty
	if caching, ok := req["enable_prompt_caching"].(bool); ok && caching {
		delete(req, "enable_prompt_caching")
	}

	return json.Marshal(req)
}
func (p *RequestyProvider) PrepareCompletionRequest(prompt string, options map[string]interface{}) ([]byte, error) {
	req := p.baseRequest(options)

	req["prompt"] = prompt

	if stream, ok := req["stream"].(bool); ok && stream {
		req["stream"] = true
	}

	return json.Marshal(req)
}
func (p *RequestyProvider) PrepareRequestWithSchema(prompt string, options map[string]interface{}, schema interface{}) ([]byte, error) {
	optsCopy := make(map[string]interface{}, len(options)+1)
	for k, v := range options {
		optsCopy[k] = v
	}
	optsCopy["response_format"] = map[string]interface{}{
		"type":   "json_object",
		"schema": schema,
	}

	// reuse normal chat request path
	return p.PrepareRequest(prompt, optsCopy)
}
func (p *RequestyProvider) PrepareRequestWithMessages(messages []types.MemoryMessage, options map[string]interface{}) ([]byte, error) {
	req := p.baseRequest(options)

	req["messages"] = p.buildMessagesFromMemory(messages, req)

	// tools / tool_choice, stream as above
	if tools, ok := req["tools"].([]interface{}); ok && len(tools) > 0 {
		req["tools"] = tools
	}
	if toolChoice, ok := req["tool_choice"]; ok {
		req["tool_choice"] = toolChoice
	}
	if stream, ok := req["stream"].(bool); ok && stream {
		req["stream"] = true
	}

	// prompt caching handled in buildMessagesFromMemory; drop flag
	delete(req, "enable_prompt_caching")

	return json.Marshal(req)
}

PrepareStreamRequest can also reuse baseRequest or just wrap PrepareRequest as you already do.

2. Factor message-building (single prompt vs memory)

You can reuse the same multimodal/image handling and system-message behavior between prompt-based and memory-based calls.

func (p *RequestyProvider) buildMessagesForPrompt(prompt string, req map[string]interface{}) []map[string]interface{} {
	var messages []map[string]interface{}

	if sysMsg, ok := req["system_message"].(string); ok {
		messages = append(messages, map[string]interface{}{
			"role":    "system",
			"content": sysMsg,
		})
		delete(req, "system_message")
	}

	// multimodal user message
	if images, ok := req["images"].([]types.ContentPart); ok && len(images) > 0 {
		contentArray := []map[string]interface{}{
			{"type": "text", "text": prompt},
		}
		contentArray = append(contentArray, ConvertImagesToOpenAIContent(images)...)
		delete(req, "images")

		messages = append(messages, map[string]interface{}{
			"role":    "user",
			"content": contentArray,
		})
	} else {
		messages = append(messages, map[string]interface{}{
			"role":    "user",
			"content": prompt,
		})
	}

	return messages
}
func (p *RequestyProvider) buildMessagesFromMemory(messages []types.MemoryMessage, req map[string]interface{}) []map[string]interface{} {
	formatted := make([]map[string]interface{}, 0, len(messages))
	for _, msg := range messages {
		formattedMsg := map[string]interface{}{"role": msg.Role}

		if msg.HasMultiContent() {
			formattedMsg["content"] = BuildOpenAIContentFromParts(msg.MultiContent)
		} else {
			formattedMsg["content"] = msg.Content
			formattedMsg["content"] = p.applyPromptCachingIfNeeded(msg, req, formattedMsg["content"])
		}

		formatted = append(formatted, formattedMsg)
	}

	// last user message + images (reusing existing helper)
	if images, ok := req["images"].([]types.ContentPart); ok && len(images) > 0 {
		for i := len(formatted) - 1; i >= 0; i-- {
			if formatted[i]["role"] == "user" {
				contentArray := NormalizeContentArray(formatted[i]["content"])
				contentArray = append(contentArray, ConvertImagesToOpenAIContent(images)...)
				formatted[i]["content"] = contentArray
				break
			}
		}
		delete(req, "images")
	}

	return formatted
}

And isolate the Anthropic prompt-caching mutation:

func (p *RequestyProvider) applyPromptCachingIfNeeded(msg types.MemoryMessage, req map[string]interface{}, content interface{}) interface{} {
	caching, ok := req["enable_prompt_caching"].(bool)
	if !ok || !caching || msg.Role != "user" {
		return content
	}
	text, ok := msg.Content.(string)
	if !ok || len(text) <= 1000 {
		return content
	}
	if !strings.HasPrefix(p.model, "anthropic/") {
		return content
	}
	return []map[string]interface{}{
		{
			"type": "text",
			"text": text,
			"cache_control": map[string]string{
				"type": "ephemeral",
			},
		},
	}
}

This keeps the main message loops straightforward and moves provider-specific heuristics into a single, testable function.

3. Share response types between parsing functions

ParseResponse, HandleFunctionCalls, and ParseStreamResponse each define similar anonymous structs. These can be lifted to private types at the top of the file:

type requestyChatResponse struct {
	Choices []struct {
		Message struct {
			Content      string           `json:"content"`
			FunctionCall *json.RawMessage `json:"function_call"`
			ToolCalls    []struct {
				ID       string `json:"id"`
				Type     string `json:"type"`
				Function struct {
					Name      string          `json:"name"`
					Arguments json.RawMessage `json:"arguments"`
				} `json:"function"`
			} `json:"tool_calls"`
		} `json:"message"`
		FinishReason       string `json:"finish_reason"`
		NativeFinishReason string `json:"native_finish_reason"`
	} `json:"choices"`
	Error struct {
		Message string `json:"message"`
	} `json:"error"`
	ID    string `json:"id"`
	Model string `json:"model"`
}

type requestyTextResponse struct {
	Choices []struct {
		Text string `json:"text"`
	} `json:"choices"`
	Error struct {
		Message string `json:"message"`
	} `json:"error"`
	ID    string `json:"id"`
	Model string `json:"model"`
}

type requestyStreamChunk struct {
	Choices []struct {
		Delta struct {
			Content   string `json:"content"`
			ToolCalls []struct {
				ID       string `json:"id"`
				Type     string `json:"type"`
				Function struct {
					Name      string          `json:"name"`
					Arguments json.RawMessage `json:"arguments"`
				} `json:"function"`
			} `json:"tool_calls"`
		} `json:"delta"`
		FinishReason string `json:"finish_reason"`
	} `json:"choices"`
	Error struct {
		Message string `json:"message"`
	} `json:"error"`
	ID    string `json:"id"`
	Model string `json:"model"`
	Usage *struct {
		PromptTokens     int `json:"prompt_tokens"`
		CompletionTokens int `json:"completion_tokens"`
		TotalTokens      int `json:"total_tokens"`
	} `json:"usage"`
}

Usage:

func (p *RequestyProvider) ParseResponse(body []byte) (string, error) {
	var chatResp requestyChatResponse
	chatErr := json.Unmarshal(body, &chatResp)
	// ... unchanged logic ...
	var textResp requestyTextResponse
	// ...
}
func (p *RequestyProvider) HandleFunctionCalls(body []byte) ([]byte, error) {
	var resp requestyChatResponse
	// ...
}
func (p *RequestyProvider) ParseStreamResponse(chunk []byte) (string, error) {
	var resp requestyStreamChunk
	// ...
}

This reduces repetition and centralizes any future changes to the response shape.

All of these changes keep the existing behavior but substantially trim the file, reduce duplication, and make future modifications (especially around options handling and response formats) safer and more consistent.

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