c-point-labs / laravel-hostyourai
HostYourAI for Laravel: a Prism provider for chat and a typed client for the decision API. Not affiliated with HostYourAI B.V.
Package info
github.com/C-Point-Labs/laravel-hostyourai
pkg:composer/c-point-labs/laravel-hostyourai
Requires
- php: ^8.2
- composer-runtime-api: ^2.2
- illuminate/http: ^12.61.1|^13.12.0
- illuminate/support: ^12.61.1|^13.12.0
- prism-php/prism: ^0.102
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.13
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- shipmonk/phpstan-rules: ^4.0
- tomasvotruba/type-coverage: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Install · Chat · Decisions · Errors · Testing
Use HostYourAI from Laravel: chat through Prism, and typed judgments your code can act on.
use CPointLabs\HostYourAI\Decisions\Questions\Noul; use CPointLabs\HostYourAI\Facades\HostYourAI; use Prism\Prism\Facades\Prism; // Generate text, stream it, call tools or get structured output. $reply = Prism::text() ->using('hostyourai', 'hyai/auto') ->withPrompt('Write a short reply to this customer.') ->asText(); // Or ask a question and get a number back instead of text. $decision = HostYourAI::decision() ->state('I was charged twice for the same invoice.') ->ask('refund', Noul::make('Is a refund requested?')) ->send(); if ($decision->noul('refund')->isYes(0.8)) { // Route it to billing. }
| Chat | A Prism provider with text, streaming, tool calling and structured output |
| Decisions | A fluent client with readonly DTOs for Choice, Noul and Score questions |
| Errors | Typed exceptions for API errors, with Retry-After support |
| Privacy | This package logs nothing, and reasoning text stays away from your users by default |
| Testing | A fake for decisions, next to Prism's own |
This is an independent open source package. It is not affiliated with HostYourAI B.V.
Install
Requires PHP 8.2+ and Laravel 12.61+ or 13.12+. You also need a HostYourAI account and key: register, then create one under API keys.
composer require c-point-labs/laravel-hostyourai
Add your key to .env:
HOSTYOURAI_API_KEY=your-key # Optional, this is the default: HOSTYOURAI_BASE_URL=https://hostyourai.com/api/v1
To change timeouts, retries or the default decision model, publish the config:
php artisan vendor:publish --tag=hostyourai-config
| Key | Default | Meaning |
|---|---|---|
api_key |
HOSTYOURAI_API_KEY |
Your API key |
base_url |
HOSTYOURAI_BASE_URL |
URL for chat, models and balance |
decision_url |
derived from base_url |
URL for the decision API |
timeout |
30 |
Seconds per request. For a stream, the wait for the response to start |
connect_timeout |
5 |
Seconds to connect. PHP's stream transport ignores this for streams, where timeout applies |
stream_timeout |
120 |
Total seconds a stream may run, also when it stalls |
retry.times |
0 |
Automatic retries on 429 and 503 |
retry.max_wait |
5 |
Longest Retry-After, in seconds, that is still retried |
decision_model |
hyai/loes-beslis |
Model used when you don't call ->model() |
You don't need an entry in config/prism.php.
Chat
Use Prism with the hostyourai provider. hyai/auto lets the router pick a model.
use Prism\Prism\Facades\Prism; $response = Prism::text() ->using('hostyourai', 'hyai/auto') ->withSystemPrompt('Answer briefly.') ->withPrompt('What is the capital of the Netherlands?') ->withMaxTokens(300) ->asText(); echo $response->text; echo $response->meta->model; // The model that answered
Streaming
VisibleText gives you only the text meant for the user:
use CPointLabs\HostYourAI\Prism\VisibleText; $stream = Prism::text() ->using('hostyourai', 'hyai/auto') ->withPrompt('Tell me about Groningen.') ->asStream(); foreach (VisibleText::from($stream) as $delta) { echo $delta; }
Or handle every Prism event yourself:
use Prism\Prism\Streaming\Events\TextDeltaEvent; foreach ($stream as $event) { if ($event instanceof TextDeltaEvent) { echo $event->delta; } }
A stream that ends early or runs past stream_timeout throws a ConnectionException, so you can show a fallback message.
Reasoning
Some models reason before they answer. That text is left out unless you ask for it:
$response->textand text deltas hold the answer only. Leading blank lines are removed.- Text and structured responses never include reasoning.
- Streams, including
asEventStreamResponse(),asDataStreamResponse()andasBroadcast(), leave it out by default.
To receive it in a stream as Prism's ThinkingEvent, turn it on. Prism's stream responses then send it to the browser too:
Prism::text() ->using('hostyourai', 'hyai/auto') ->withProviderOptions(['reasoning' => true]) ->withPrompt('Hello') ->asStream();
Reasoning can use up your max_tokens. You then get empty text and FinishReason::Length, so allow for this when you set a low limit.
Provider options
hyai.task and hyai.policy tell HostYourAI's router about a request, on text, structured output and streams:
Prism::text() ->using('hostyourai', 'hyai/auto') ->withProviderOptions(['hyai' => ['task' => 'summarize', 'policy' => 'cost']]) ->withPrompt('Summarize this ticket.') ->asText();
task:chat,code,summarize,classify,extract,rag,legalorreason.policy:balanced,speed,cost,qualityorsovereign.
Streams also take reasoning, and structured output takes schema.strict. Any other option or value raises an InvalidRequestException that names the option, instead of being ignored.
Tool calling
use Prism\Prism\Facades\Tool; $weather = Tool::as('weather') ->for('Get the current weather') ->withStringParameter('city', 'The city') ->using(fn (string $city): string => "It is 18 degrees in {$city}."); $response = Prism::text() ->using('hostyourai', 'hyai/auto') ->withTools([$weather]) ->withMaxSteps(3) ->withPrompt('What is the weather in Groningen?') ->asText();
HostYourAI::models() shows which models support tools.
Structured output
use Prism\Prism\Schema\BooleanSchema; use Prism\Prism\Schema\ObjectSchema; $schema = new ObjectSchema('result', 'The result', [ new BooleanSchema('in_europe', 'Is it in Europe?'), ], ['in_europe']); $response = Prism::structured() ->using('hostyourai', 'mistral-small-4-119b') ->withSchema($schema) ->withPrompt('Is Groningen in Europe?') ->withMaxTokens(100) ->asStructured(); $response->structured['in_europe'];
- Use a named model and test it for your schema: response times and support differ per model, and
hyai/automay pick a slower one. - Invalid or cut-off output throws a
StructuredOutputException. - Set
withProviderOptions(['schema' => ['strict' => false]])to turn off strict mode. - Tools can't be combined with structured output in this provider.
Decisions
The decision API returns typed judgments instead of text: your code gets a choice, a probability or a score. HostYourAI labels the decision API and Loes Beslis as beta, so they may still change.
use CPointLabs\HostYourAI\Decisions\Questions\Choice; use CPointLabs\HostYourAI\Decisions\Questions\Noul; use CPointLabs\HostYourAI\Decisions\Questions\Score; use CPointLabs\HostYourAI\Facades\HostYourAI; $result = HostYourAI::decision() ->model('hyai/loes-beslis') ->state('I was charged twice for the same invoice.') ->ask('team', Choice::make('Which team handles this?', [ 'billing' => 'Invoices and payments', 'technical' => 'Bugs and integrations', 'other' => 'Anything else', ])) ->ask('refund', Noul::make('Is a refund requested?')) ->ask('urgency', Score::make('How urgent is this?', ['Can wait', 'Today', 'Immediately'])) ->send(); $result->choice('team')->choice; // 'billing' $result->choice('team')->confidence; // 0.88 $result->choice('team')->probabilities; // ['billing' => 0.92, ...] $result->noul('refund')->probability; // 0.24 $result->noul('refund')->isYes(0.8); // false $result->score('urgency')->score; // 1.7 $result->score('urgency')->confidence; // 0.55 $result->score('urgency')->legend; // [0 => 'Can wait', ...] $result->score('urgency')->probabilities; $result->usage->inputTokens; $result->costCredits;
state()takes a string or an array.Noul::make($question, yes: '...', no: '...')spells out what counts as yes or no.->timeout(5)sets the timeout for one call.- The builder checks the limits before sending: 2 to 16 choice options, 1 to 10 score levels and at most 16 questions.
A probability is not a guarantee, so validate your thresholds on your own data.
Models can be cold, and limits differ per model. Check them first:
$ready = HostYourAI::decisionModels()->where('ready', true); $ready->first()->id; $ready->first()->contextTokensPerQuestion;
Ensembles
Ensembles are not supported yet.
Account
$balance = HostYourAI::balance(); $balance->balance; // 41.25 $balance->spendMonth; // 12.80 foreach (HostYourAI::models() as $model) { $model->id; $model->contextLength; $model->supportsTools; $model->supportsImages; }
balance() needs a personal key.
Errors
Every API error is a typed exception that extends HostYourAIException, which extends Prism's PrismException. This holds for decisions and for Prism calls.
| Exception | When |
|---|---|
AuthenticationException |
401: the key is missing, invalid or revoked |
BudgetExceededException |
402: credit or team budget used up |
ModelNotAllowedException |
403: this key may not use the model |
PersonalKeyRequiredException |
403: the endpoint needs a personal key |
SovereigntyViolationException |
403: EU only is on and the machine that got this request is not sovereign |
InvalidRequestException |
400, 404, 413, 422 |
RateLimitedException |
429 |
ModelUnavailableException |
503: the model can't serve this request right now |
SovereigntyUnavailableException |
503: EU only is on and the model has no sovereign capacity right now |
RouterUnavailableException |
503: the team's router could not be read, so the request was refused rather than served without its guards |
ServerException |
Other 5xx |
ConnectionException |
Network failure, timeout, or a stream that ended early |
ConfigurationException |
No API key configured |
UnexpectedResponseException |
A response the package can't read |
StructuredOutputException |
Invalid or incomplete structured output |
An error this package doesn't recognize stays a plain HostYourAIException, with the API's status and errorCode. SovereigntyUnavailableException and RouterUnavailableException extend ModelUnavailableException, so existing catch blocks keep working.
They all extend Prism's PrismException, but not its more specific types: catch RateLimitedException here, not PrismRateLimitedException.
Each one has status, errorType, errorCode, requestId, retryAfter and retryAt(). retryAfter is null when the API sends no Retry-After.
use CPointLabs\HostYourAI\Exceptions\HostYourAIException; use CPointLabs\HostYourAI\Exceptions\ModelUnavailableException; try { $answer = Prism::text()->using('hostyourai', 'hyai/auto')->withPrompt($prompt)->asText()->text; } catch (ModelUnavailableException $e) { $tryAgainAt = $e->retryAt(); } catch (HostYourAIException $e) { report($e); }
Retries are off by default. With retry.times set, 429 and 503 responses are retried after Retry-After (or 1 second if absent), unless that wait is longer than retry.max_wait.
EU only and data handling
This package doesn't decide where a request runs. HostYourAI does, based on your key and team.
- Turning it on: EU only, also called Sovereignty Mode, is a switch per API key in your HostYourAI account. For a team, set the
eu_onlyguard on its router through the Management API. The security page also describes conditions for activating it per account, so check with HostYourAI if you depend on it. - EU-hosted and sovereign: EU-hosted says where the machine is. HostYourAI's security page names US-incorporated GPU marketplaces whose machines are in the EU. Sovereign capacity is limited to sub-processors with an EU establishment and EU-only processing locations.
- Policy
sovereign:withProviderOptions(['hyai' => ['policy' => 'sovereign']])is a routing preference. EU only is what enforces, so don't rely on the policy alone for data that must stay in the EU. Model::$sovereigntySafe:HostYourAI::models()exposes the API'ssovereignty_safevalue, ornullwhen it isn't sent. HostYourAI doesn't document this field publicly, so treat it as a hint when choosing a model. It enforces nothing: EU only does.- Retention: how long HostYourAI keeps data is set by its own terms, not by this package. See Logging & retention.
When EU only or a team's router refuses a request, don't fall back to a provider outside the EU. That would send the data exactly where the setting says it may not go:
use CPointLabs\HostYourAI\Exceptions\ModelUnavailableException; use CPointLabs\HostYourAI\Exceptions\RouterUnavailableException; use CPointLabs\HostYourAI\Exceptions\SovereigntyUnavailableException; use CPointLabs\HostYourAI\Exceptions\SovereigntyViolationException; try { $answer = Prism::text()->using('hostyourai', 'hyai/auto')->withPrompt($prompt)->asText()->text; } catch (SovereigntyUnavailableException | SovereigntyViolationException | RouterUnavailableException $e) { // Show a message or try again later. No fallback outside the EU. } catch (ModelUnavailableException $e) { // The model is busy or cold: retry at $e->retryAt() or pick another model. }
Privacy
The package doesn't log anything. Prompts and keys never appear in its exception messages, and the key and decision state are marked as #[\SensitiveParameter], so PHP hides them from stack traces. Your own logging, error tracking and HTTP monitoring are still yours to check.
Testing your app
Decisions, balance and models:
use CPointLabs\HostYourAI\Exceptions\BudgetExceededException; use CPointLabs\HostYourAI\Facades\HostYourAI; $fake = HostYourAI::fake([ 'refund' => 0.9, // Noul or Score 'team' => 'billing', // Choice ]); // ... $fake->assertDecided(fn ($request) => $request->state === 'Refund please'); $fake->assertDecisionCount(1); $fake->assertNothingDecided(); HostYourAI::fake(new BudgetExceededException('Budget used up', 402)); HostYourAI::fake(fn ($request) => ['refund' => 0.1]);
Chat uses Prism's own Prism::fake().
Development
composer install composer check
composer check runs Pint, PHPStan and Pest. PHPStan runs at level max with bleeding edge, strict rules, deprecation rules, Larastan, ShipMonk rules and type coverage, with no baseline.
A small live smoke test calls the real API with neutral text and low token limits. It is skipped unless you ask for it:
HOSTYOURAI_LIVE_TESTS=true HOSTYOURAI_API_KEY=your-key vendor/bin/pest tests/Live
HOSTYOURAI_LIVE_TESTS and HOSTYOURAI_API_KEY must be in the actual process environment. Some local PHP setups, like Lerd, don't pass environment variables through to PHP, and the live tests are then skipped.
Getting help
- Problems with this package: GitHub Issues.
- Problems with the API or your account: HostYourAI support.
License
MIT. See LICENSE.