Skip to content

Commit 942f6d7

Browse files
committed
feat: DSN configuration
1 parent 502ad53 commit 942f6d7

File tree

9 files changed

+280
-11
lines changed

9 files changed

+280
-11
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,53 @@ $instructrice->get(
276276
);
277277
```
278278

279+
#### DSN
280+
281+
You may configure the LLM using a DSN:
282+
- the scheme is the provider: `openai`, `openai-http`, `anthropic`, `google`
283+
- the password is the api key
284+
- the host, port and path are the api endpoints without the scheme
285+
- the query string:
286+
- `model` is the model name
287+
- `context` is the context window
288+
- `strategy` is the strategy to use:
289+
- `json` for json mode with the schema in the prompt only
290+
- `json_with_schema` for json mode with probably the completion perfectly constrained to the schema
291+
- `tool_any`
292+
- `tool_auto`
293+
- `tool_function`
294+
295+
Examples:
296+
```php
297+
use AdrienBrault\Instructrice\InstructriceFactory;
298+
299+
$instructrice = InstructriceFactory::create(
300+
defaultLlm: 'openai://:[email protected]/v1/chat/completions?model=gpt-3.5-turbo&strategy=tool_auto&context=16000'
301+
);
302+
303+
$instructrice->get(
304+
...,
305+
llm: 'openai-http://localhost:11434?model=adrienbrault/nous-hermes2theta-llama3-8b&strategy=json&context=8000'
306+
);
307+
308+
$instructrice->get(
309+
...,
310+
llm: 'openai://:[email protected]/inference/v1/chat/completions?model=accounts/fireworks/models/llama-v3-70b-instruct&context=8000&strategy=json_with_schema'
311+
);
312+
313+
$instructrice->get(
314+
...,
315+
llm: 'google://:[email protected]/v1beta/models?model=gemini-1.5-flash&context=1000000'
316+
);
317+
318+
$instructrice->get(
319+
...,
320+
llm: 'anthropic://:[email protected]?model=claude-3-haiku-20240307&context=200000'
321+
);
322+
```
323+
324+
#### LLMInterface
325+
279326
You may also implement [LLMInterface](src/LLM/LLMInterface.php).
280327

281328
## Acknowledgements

src/Instructrice.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
class Instructrice
2828
{
2929
public function __construct(
30-
private readonly ProviderModel|LLMConfig $defaultLlm,
30+
private readonly ProviderModel|LLMConfig|string $defaultLlm,
3131
private readonly LLMFactory $llmFactory,
3232
private readonly LoggerInterface $logger,
3333
private readonly SchemaFactory $schemaFactory,
@@ -50,7 +50,7 @@ public function get(
5050
?string $prompt = null,
5151
array $options = [],
5252
?callable $onChunk = null,
53-
LLMInterface|LLMConfig|ProviderModel|null $llm = null,
53+
LLMInterface|LLMConfig|ProviderModel|string|null $llm = null,
5454
) {
5555
$denormalize = fn (mixed $data) => $data;
5656
$schema = $type;
@@ -89,7 +89,7 @@ public function list(
8989
?string $prompt = null,
9090
array $options = [],
9191
?callable $onChunk = null,
92-
LLMInterface|LLMConfig|ProviderModel|null $llm = null,
92+
LLMInterface|LLMConfig|ProviderModel|string|null $llm = null,
9393
): array {
9494
$wrappedWithProperty = 'list';
9595
$schema = [
@@ -145,7 +145,7 @@ private function getAndDenormalize(
145145
string $prompt,
146146
bool $truncateAutomatically = false,
147147
?callable $onChunk = null,
148-
LLMInterface|LLMConfig|ProviderModel|null $llm = null,
148+
LLMInterface|LLMConfig|ProviderModel|string|null $llm = null,
149149
): mixed {
150150
if (($schema['type'] ?? null) !== 'object') {
151151
$wrappedWithProperty = 'inner';

src/LLM/Cost.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
class Cost
88
{
99
public function __construct(
10-
public readonly float $millionPromptTokensPrice,
11-
public readonly float $millionCompletionTokensPrice,
10+
public readonly float $millionPromptTokensPrice = 0,
11+
public readonly float $millionCompletionTokensPrice = 0,
1212
) {
1313
}
1414

src/LLM/DSNParser.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AdrienBrault\Instructrice\LLM;
6+
7+
use AdrienBrault\Instructrice\LLM\Client\AnthropicLLM;
8+
use AdrienBrault\Instructrice\LLM\Client\GoogleLLM;
9+
use AdrienBrault\Instructrice\LLM\Client\OpenAiLLM;
10+
use InvalidArgumentException;
11+
12+
use function Psl\Type\int;
13+
use function Psl\Type\literal_scalar;
14+
use function Psl\Type\optional;
15+
use function Psl\Type\shape;
16+
use function Psl\Type\string;
17+
use function Psl\Type\union;
18+
19+
class DSNParser
20+
{
21+
public function parse(string $dsn): LLMConfig
22+
{
23+
$parsedUrl = parse_url($dsn);
24+
25+
if (! \is_array($parsedUrl)) {
26+
throw new InvalidArgumentException('The DSN could not be parsed');
27+
}
28+
29+
$parsedUrl = shape([
30+
'scheme' => string(),
31+
'pass' => optional(string()),
32+
'host' => string(),
33+
'port' => optional(int()),
34+
'path' => optional(string()),
35+
'query' => string(),
36+
], true)->coerce($parsedUrl);
37+
38+
$apiKey = $parsedUrl['pass'] ?? null;
39+
$host = $parsedUrl['host'];
40+
$port = $parsedUrl['port'] ?? null;
41+
$path = $parsedUrl['path'] ?? null;
42+
$query = $parsedUrl['query'];
43+
44+
$hostWithPort = $host . ($port === null ? '' : ':' . $port);
45+
46+
$client = union(
47+
literal_scalar('openai'),
48+
literal_scalar('openai-http'),
49+
literal_scalar('anthropic'),
50+
literal_scalar('google')
51+
)->coerce($parsedUrl['scheme']);
52+
53+
parse_str($query, $parsedQuery);
54+
$model = $parsedQuery['model'];
55+
$strategyName = $parsedQuery['strategy'] ?? null;
56+
$context = (int) ($parsedQuery['context'] ?? null);
57+
58+
if (! \is_string($model)) {
59+
throw new InvalidArgumentException('The DSN "model" query string must be a string');
60+
}
61+
62+
if ($context <= 0) {
63+
throw new InvalidArgumentException('The DSN "context" query string must be a positive integer');
64+
}
65+
66+
$scheme = 'https';
67+
68+
$strategy = null;
69+
if ($strategyName === 'json') {
70+
$strategy = OpenAiJsonStrategy::JSON;
71+
} elseif ($strategyName === 'json_with_schema') {
72+
$strategy = OpenAiJsonStrategy::JSON_WITH_SCHEMA;
73+
} elseif ($strategyName === 'tool_any') {
74+
$strategy = OpenAiToolStrategy::ANY;
75+
} elseif ($strategyName === 'tool_auto') {
76+
$strategy = OpenAiToolStrategy::AUTO;
77+
} elseif ($strategyName === 'tool_function') {
78+
$strategy = OpenAiToolStrategy::FUNCTION;
79+
}
80+
81+
if ($client === 'anthropic') {
82+
$headers = [
83+
'x-api-key' => $apiKey,
84+
];
85+
$llmClass = AnthropicLLM::class;
86+
$path ??= '/v1/messages';
87+
} elseif ($client === 'google') {
88+
$headers = [
89+
'x-api-key' => $apiKey,
90+
];
91+
$llmClass = GoogleLLM::class;
92+
$path ??= '/v1beta/models';
93+
} elseif ($client === 'openai' || $client === 'openai-http') {
94+
$path ??= '/v1/chat/completions';
95+
$headers = $apiKey === null ? [] : [
96+
'Authorization' => 'Bearer ' . $apiKey,
97+
];
98+
99+
$llmClass = OpenAiLLM::class;
100+
101+
if ($client === 'openai-http') {
102+
$scheme = 'http';
103+
}
104+
} else {
105+
throw new InvalidArgumentException(sprintf('Unknown client "%s", use one of %s', $client, implode(', ', ['openai', 'anthropic', 'google'])));
106+
}
107+
108+
$uri = $scheme . '://' . $hostWithPort . $path;
109+
110+
return new LLMConfig(
111+
$uri,
112+
$model,
113+
$context,
114+
$model,
115+
$hostWithPort,
116+
new Cost(),
117+
$strategy,
118+
headers: $headers,
119+
llmClass: $llmClass
120+
);
121+
}
122+
}

src/LLM/LLMConfig.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class LLMConfig
1515
* @param callable(mixed, string): string $systemPrompt
1616
* @param array<string, mixed> $headers
1717
* @param list<string> $stopTokens
18+
* @param class-string<LLMInterface> $llmClass
1819
*/
1920
public function __construct(
2021
public readonly string $uri,
@@ -29,6 +30,7 @@ public function __construct(
2930
public readonly ?int $maxTokens = null,
3031
public readonly ?string $docUrl = null,
3132
array|false|null $stopTokens = null,
33+
public readonly ?string $llmClass = null,
3234
) {
3335
if ($stopTokens !== false) {
3436
$this->stopTokens = $stopTokens ?? ["```\n\n", '<|im_end|>', "\n\n\n", "\t\n\t\n"];

src/LLM/LLMFactory.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,23 @@ public function __construct(
4242
private readonly array $apiKeys = [],
4343
private readonly Gpt3Tokenizer $tokenizer = new Gpt3Tokenizer(new Gpt3TokenizerConfig()),
4444
private readonly ParserInterface $parser = new JsonParser(),
45+
private readonly DSNParser $dsnParser = new DSNParser(),
4546
) {
4647
}
4748

48-
public function create(LLMConfig|ProviderModel $config): LLMInterface
49+
public function create(LLMConfig|ProviderModel|string $config): LLMInterface
4950
{
51+
if (\is_string($config)) {
52+
$config = $this->dsnParser->parse($config);
53+
}
54+
5055
if ($config instanceof ProviderModel) {
5156
$apiKey = $this->apiKeys[$config::class] ?? null;
5257
$apiKey ??= self::getProviderModelApiKey($config, true) ?? 'sk-xxx';
5358
$config = $config->createConfig($apiKey);
5459
}
5560

56-
if (str_contains($config->uri, 'api.anthropic.com')) {
61+
if ($config->llmClass === AnthropicLLM::class) {
5762
return new AnthropicLLM(
5863
$this->client,
5964
$this->logger,
@@ -62,7 +67,7 @@ public function create(LLMConfig|ProviderModel $config): LLMInterface
6267
);
6368
}
6469

65-
if (str_contains($config->uri, 'googleapis.com')) {
70+
if ($config->llmClass === GoogleLLM::class) {
6671
return new GoogleLLM(
6772
$config,
6873
$this->client,
@@ -72,6 +77,10 @@ public function create(LLMConfig|ProviderModel $config): LLMInterface
7277
);
7378
}
7479

80+
if ($config->llmClass !== OpenAiLLM::class && $config->llmClass !== null) {
81+
throw new InvalidArgumentException(sprintf('Unknown LLM class %s', $config->llmClass));
82+
}
83+
7584
return new OpenAiLLM(
7685
$config,
7786
$this->client,

src/LLM/Provider/Anthropic.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace AdrienBrault\Instructrice\LLM\Provider;
66

7+
use AdrienBrault\Instructrice\LLM\Client\AnthropicLLM;
78
use AdrienBrault\Instructrice\LLM\Cost;
89
use AdrienBrault\Instructrice\LLM\LLMConfig;
910

@@ -63,7 +64,8 @@ public function createConfig(string $apiKey): LLMConfig
6364
headers: [
6465
'x-api-key' => $apiKey,
6566
],
66-
docUrl: 'https://docs.anthropic.com/claude/docs/models-overview'
67+
docUrl: 'https://docs.anthropic.com/claude/docs/models-overview',
68+
llmClass: AnthropicLLM::class,
6769
);
6870
}
6971
}

src/LLM/Provider/Google.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace AdrienBrault\Instructrice\LLM\Provider;
66

7+
use AdrienBrault\Instructrice\LLM\Client\GoogleLLM;
78
use AdrienBrault\Instructrice\LLM\Cost;
89
use AdrienBrault\Instructrice\LLM\LLMConfig;
910

@@ -36,7 +37,8 @@ public function createConfig(string $apiKey): LLMConfig
3637
headers: [
3738
'x-api-key' => $apiKey,
3839
],
39-
docUrl: 'https://ai.google.dev/gemini-api/docs/models/gemini'
40+
docUrl: 'https://ai.google.dev/gemini-api/docs/models/gemini',
41+
llmClass: GoogleLLM::class,
4042
);
4143
}
4244
}

0 commit comments

Comments
 (0)