Skip to content

Commit f1e1c68

Browse files
committed
Add support for client providers
1 parent d62dea9 commit f1e1c68

21 files changed

Lines changed: 3423 additions & 140 deletions

examples/graphics/source/examples/AI.h

Lines changed: 313 additions & 79 deletions
Large diffs are not rendered by default.

modules/yup_ai/llm/yup_LLMClient.cpp

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ LLMResponse LLMClient::runToolLoop (const Request& request, LLMToolRegistry& too
9191
for (const auto& toolCall : response.getToolCalls())
9292
{
9393
auto result = tools.dispatchToolCall (toolCall.name, toolCall.arguments);
94-
current.messages.push_back (LLMMessage::toolResult (toolCall.id, JSON::toString (result, true)));
94+
95+
auto toolResultMsg = LLMMessage::toolResult (toolCall.id, JSON::toString (result, true));
96+
toolResultMsg.name = toolCall.name; // preserved for providers that need name + id separately (e.g. Gemini)
97+
current.messages.push_back (std::move (toolResultMsg));
9598
}
9699

97100
response = complete (current);
@@ -124,14 +127,19 @@ String LLMClient::buildChatCompletionBody (const Request& request, bool stream)
124127
if (request.toolChoice.has_value())
125128
setLLMClientProperty (object, "tool_choice", toolChoiceToVar (*request.toolChoice));
126129

127-
if (request.temperature.has_value())
128-
setLLMClientProperty (object, "temperature", static_cast<double> (*request.temperature));
130+
if (! options.noTemperature)
131+
{
132+
if (request.temperature.has_value())
133+
setLLMClientProperty (object, "temperature", static_cast<double> (*request.temperature));
134+
}
129135

130136
if (request.topP.has_value())
131137
setLLMClientProperty (object, "top_p", static_cast<double> (*request.topP));
132138

133-
if (request.maxTokens.has_value())
134-
setLLMClientProperty (object, "max_tokens", *request.maxTokens);
139+
// Per-request maxTokens overrides options.maxTokens; use max_completion_tokens for OpenAI-compatible APIs.
140+
const int effectiveMaxTokens = request.maxTokens.value_or (options.maxTokens);
141+
if (effectiveMaxTokens > 0)
142+
setLLMClientProperty (object, "max_completion_tokens", effectiveMaxTokens);
135143

136144
if (request.stopSequences.has_value())
137145
{
@@ -143,6 +151,40 @@ String LLMClient::buildChatCompletionBody (const Request& request, bool stream)
143151
setLLMClientProperty (object, "stop", stop);
144152
}
145153

154+
// Reasoning effort for o-series / GPT-5 models.
155+
if (options.reasoningEffort.isNotEmpty())
156+
setLLMClientProperty (object, "reasoning_effort", options.reasoningEffort);
157+
158+
// GBNF grammar for llama-server constrained decoding (per-request overrides config).
159+
const auto& effectiveGrammar = request.grammar.isNotEmpty() ? request.grammar : options.grammar;
160+
if (effectiveGrammar.isNotEmpty())
161+
setLLMClientProperty (object, "grammar", effectiveGrammar);
162+
163+
// Prompt caching — bucket by application identity, retain for 24h.
164+
if (options.userAgent.isNotEmpty())
165+
{
166+
setLLMClientProperty (object, "prompt_cache_key", options.userAgent);
167+
setLLMClientProperty (object, "prompt_cache_retention", String ("24h"));
168+
}
169+
170+
// Structured output via JSON Schema (built with LLMSchema helpers).
171+
if (! request.schema.isVoid())
172+
{
173+
auto schemaWrapper = makeLLMClientObject();
174+
setLLMClientProperty (schemaWrapper, "name", String ("response"));
175+
setLLMClientProperty (schemaWrapper, "strict", true);
176+
setLLMClientProperty (schemaWrapper, "schema", request.schema);
177+
178+
auto responseFormat = makeLLMClientObject();
179+
setLLMClientProperty (responseFormat, "type", String ("json_schema"));
180+
setLLMClientProperty (responseFormat, "json_schema", schemaWrapper);
181+
182+
setLLMClientProperty (object, "response_format", responseFormat);
183+
}
184+
185+
// OpenRouter — application identification headers are injected at HTTP level,
186+
// but some frontends read X-Title from the body; we skip that here.
187+
146188
return JSON::toString (object, true);
147189
}
148190

modules/yup_ai/llm/yup_LLMClient.h

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,49 @@ namespace yup
3030
class YUP_API LLMClient
3131
{
3232
public:
33+
/** The LLM provider type, used by LLMClientFactory to create the correct client. */
34+
enum class Provider
35+
{
36+
OpenAIChat, ///< OpenAI Chat Completions — also works with DeepSeek, OpenRouter, Ollama, llama-server.
37+
OpenAIResponses, ///< OpenAI Responses API (GPT-5+).
38+
Anthropic, ///< Anthropic Messages API — Claude models.
39+
Gemini ///< Google Gemini generateContent API.
40+
};
41+
3342
struct Request
3443
{
3544
std::vector<LLMMessage> messages;
3645
std::optional<String> systemPrompt;
3746
std::vector<LLMTool> tools;
38-
std::optional<String> toolChoice;
47+
std::optional<String> toolChoice; ///< "auto", "none", "required", or a specific function name.
3948

4049
std::optional<float> temperature;
4150
std::optional<float> topP;
42-
std::optional<int> maxTokens;
51+
std::optional<int> maxTokens; ///< Per-request override; falls back to Options::maxTokens.
4352
std::optional<std::vector<String>> stopSequences;
53+
54+
var schema; ///< Optional JSON Schema for structured output (built with LLMSchema).
55+
String grammar; ///< Optional per-request GBNF (llama-server) or Lark (OpenAI Responses) grammar.
56+
String grammarToolName; ///< Tool name for grammar-constrained output (OpenAI Responses API only).
57+
String grammarToolDescription; ///< Tool description for grammar output; defaults to system prompt if empty.
4458
};
4559

4660
struct Options
4761
{
62+
Provider provider = Provider::OpenAIChat; ///< LLM backend provider — used by LLMClientFactory.
63+
4864
String model;
4965
String baseUrl = "http://localhost:11434/v1";
5066
String apiKey;
5167
int timeoutMs = 120000;
5268
int maxRetries = 2;
69+
int maxTokens = 0; ///< Default max output tokens (0 = provider default); per-request value overrides.
70+
71+
String reasoningEffort; ///< "none", "low", "medium", "high" — for OpenAI o-series and Gemini 2.5 models.
72+
String grammar; ///< Default GBNF grammar for llama-server constrained decoding (per-request overrides).
73+
bool noTemperature = false; ///< Set true for models that reject the temperature parameter (e.g. GPT-5 series).
74+
String userAgent; ///< Application identifier used for User-Agent header and prompt cache key.
75+
String appUrl; ///< Application URL sent as HTTP-Referer on OpenRouter requests.
5376
};
5477

5578
explicit LLMClient (Options options);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
==============================================================================
3+
4+
This file is part of the YUP library.
5+
Copyright (c) 2026 - kunitoki@gmail.com
6+
7+
YUP is an open source library subject to open-source licensing.
8+
9+
The code included in this file is provided under the terms of the ISC license
10+
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
11+
to use, copy, modify, and/or distribute this software for any purpose with or
12+
without fee is hereby granted provided that the above copyright notice and
13+
this permission notice appear in all copies.
14+
15+
YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
16+
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
17+
DISCLAIMED.
18+
19+
==============================================================================
20+
*/
21+
22+
namespace yup
23+
{
24+
25+
std::unique_ptr<LLMClient> LLMClientFactory::create (LLMClient::Options options)
26+
{
27+
switch (options.provider)
28+
{
29+
case LLMClient::Provider::OpenAIChat:
30+
return std::make_unique<LLMOpenAIChatClient> (std::move (options));
31+
32+
case LLMClient::Provider::OpenAIResponses:
33+
return std::make_unique<LLMOpenAIResponsesClient> (std::move (options));
34+
35+
case LLMClient::Provider::Anthropic:
36+
return std::make_unique<LLMAnthropicClient> (std::move (options));
37+
38+
case LLMClient::Provider::Gemini:
39+
return std::make_unique<LLMGeminiClient> (std::move (options));
40+
41+
default:
42+
jassertfalse; // Unknown provider
43+
return nullptr;
44+
}
45+
}
46+
47+
//==============================================================================
48+
std::unique_ptr<LLMClient> LLMClientFactory::openAIChat (String model,
49+
String baseUrl,
50+
String apiKey)
51+
{
52+
LLMClient::Options opts;
53+
opts.provider = LLMClient::Provider::OpenAIChat;
54+
opts.model = std::move (model);
55+
opts.baseUrl = std::move (baseUrl);
56+
opts.apiKey = std::move (apiKey);
57+
return create (std::move (opts));
58+
}
59+
60+
std::unique_ptr<LLMClient> LLMClientFactory::openAIResponses (String model,
61+
String apiKey,
62+
String baseUrl)
63+
{
64+
LLMClient::Options opts;
65+
opts.provider = LLMClient::Provider::OpenAIResponses;
66+
opts.model = std::move (model);
67+
opts.apiKey = std::move (apiKey);
68+
opts.baseUrl = std::move (baseUrl);
69+
return create (std::move (opts));
70+
}
71+
72+
std::unique_ptr<LLMClient> LLMClientFactory::anthropic (String model,
73+
String apiKey,
74+
String baseUrl)
75+
{
76+
LLMClient::Options opts;
77+
opts.provider = LLMClient::Provider::Anthropic;
78+
opts.model = std::move (model);
79+
opts.apiKey = std::move (apiKey);
80+
opts.baseUrl = std::move (baseUrl);
81+
return create (std::move (opts));
82+
}
83+
84+
std::unique_ptr<LLMClient> LLMClientFactory::gemini (String model,
85+
String apiKey,
86+
String baseUrl)
87+
{
88+
LLMClient::Options opts;
89+
opts.provider = LLMClient::Provider::Gemini;
90+
opts.model = std::move (model);
91+
opts.apiKey = std::move (apiKey);
92+
opts.baseUrl = std::move (baseUrl);
93+
return create (std::move (opts));
94+
}
95+
96+
} // namespace yup
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
==============================================================================
3+
4+
This file is part of the YUP library.
5+
Copyright (c) 2026 - kunitoki@gmail.com
6+
7+
YUP is an open source library subject to open-source licensing.
8+
9+
The code included in this file is provided under the terms of the ISC license
10+
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
11+
to use, copy, modify, and/or distribute this software for any purpose with or
12+
without fee is hereby granted provided that the above copyright notice and
13+
this permission notice appear in all copies.
14+
15+
YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
16+
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
17+
DISCLAIMED.
18+
19+
==============================================================================
20+
*/
21+
22+
namespace yup
23+
{
24+
25+
//==============================================================================
26+
/** Factory that creates the correct LLMHttpClient subclass from an Options struct.
27+
28+
Use LLMClientFactory::create() to instantiate an LLM client for any supported
29+
provider. The Provider enum in LLMClient::Options selects the concrete class.
30+
31+
@code
32+
yup::LLMClient::Options opts;
33+
opts.provider = yup::LLMClient::Provider::Anthropic;
34+
opts.model = "claude-opus-4-5";
35+
opts.apiKey = "sk-ant-...";
36+
opts.baseUrl = "https://api.anthropic.com/v1";
37+
38+
auto client = yup::LLMClientFactory::create (opts);
39+
auto response = client->chat ("Hello, Claude!");
40+
@endcode
41+
42+
Convenience static methods are provided for the most common provider setups.
43+
44+
@tags{AI}
45+
*/
46+
class YUP_API LLMClientFactory
47+
{
48+
public:
49+
/** Creates an LLM client for the provider specified in @p options.
50+
51+
@param options Full options struct. options.provider selects the concrete class.
52+
@returns A heap-allocated concrete LLMHttpClient subclass, or nullptr if
53+
the provider enum value is unrecognised.
54+
*/
55+
static std::unique_ptr<LLMClient> create (LLMClient::Options options);
56+
57+
//==============================================================================
58+
/** Convenience factory — OpenAI Chat Completions (also Ollama, DeepSeek, OpenRouter, llama-server). */
59+
static std::unique_ptr<LLMClient> openAIChat (String model,
60+
String baseUrl = "http://localhost:11434/v1",
61+
String apiKey = {});
62+
63+
/** Convenience factory — OpenAI Responses API (GPT-5+, reasoning models). */
64+
static std::unique_ptr<LLMClient> openAIResponses (String model,
65+
String apiKey,
66+
String baseUrl = "https://api.openai.com/v1");
67+
68+
/** Convenience factory — Anthropic Messages API (Claude models). */
69+
static std::unique_ptr<LLMClient> anthropic (String model,
70+
String apiKey,
71+
String baseUrl = "https://api.anthropic.com/v1");
72+
73+
/** Convenience factory — Google Gemini generateContent API. */
74+
static std::unique_ptr<LLMClient> gemini (String model,
75+
String apiKey,
76+
String baseUrl = "https://generativelanguage.googleapis.com");
77+
};
78+
79+
} // namespace yup

0 commit comments

Comments
 (0)