Search by

c-point-labs / laravel-hostyourai

mtolhuijs

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

Statistics

Installs: 35

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.3.1 2026-11-07 15:14 UTC

This package is auto-updated.

Last update: 2026-11-07 15:15:35 UTC


README

Laravel HostYourAI: Prism provider, typed decisions, EU-hosted open models

Tests Latest version PHP version PHPStan level max MIT license

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->text and text deltas hold the answer only. Leading blank lines are removed.
  • Text and structured responses never include reasoning.
  • Streams, including asEventStreamResponse(), asDataStreamResponse() and asBroadcast(), 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, legal or reason.
  • policy: balanced, speed, cost, quality or sovereign.

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/auto may 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_only guard 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's sovereignty_safe value, or null when 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

License

MIT. See LICENSE.