Looking to hire Laravel developers? Try LaraJobs
This package is not available.

laravel-hostyourai maintained by c-point-labs

Author
Maarten Tolhuijs
Last update
2026/11/06 23:57 (v0.2.0)
License
Downloads
0

Comments
comments powered by Disqus

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 every API error, with Retry-After support
Privacy No logging, 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+.

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.

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.

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
InvalidRequestException 400, 404, 413, 422
RateLimitedException 429
ModelUnavailableException 503: the model can't serve this request right now
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

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.

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.

License

MIT. See LICENSE.