Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ jobs:

- name: 🧪 test
shell: pwsh
env:
CI_XAI_API_KEY: ${{ secrets.CI_XAI_API_KEY }}
run: dnx --yes retest -- --no-build

- name: 🐛 logs
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:

- name: 🧪 test
shell: pwsh
env:
CI_XAI_API_KEY: ${{ secrets.CI_XAI_API_KEY }}
run: dnx --yes retest -- --no-build

- name: 🐛 logs
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ _site
.sass-cache
Gemfile.lock
package-lock.json

/.github/.labels
32 changes: 16 additions & 16 deletions .netconfig
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@
weak
[file ".gitignore"]
url = https://github.com/devlooped/oss/blob/main/.gitignore
sha = ff61659751374b95c7a8a0477c908a8119f756f0
etag = e5865f083db45081a7b4eaa518018971b34e6ef93f917ac510dea96d27f792b3
sha = fefbf3606e7bb7f483951e3107200a2f45ee52e0
etag = 5bdfd49876c886aaa6b7026960261af9b587e7cc3baa0f4584db34d9cebbd3f4
weak
[file "Directory.Build.rsp"]
url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp
Expand All @@ -110,13 +110,13 @@
weak
[file "src/Directory.Build.props"]
url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props
sha = 6e2438919e108aeb75106dc0737c45f5e55d5f42
etag = f1d6384abf18d8d891ce5e835a10c73fe029c42151374be96d7e4af43d189c65
sha = 59861ddd1e330f3da410652b4c7fc6585726c0cf
etag = 1561f1be53cc25ac0cd85a06b3f65d69764b174d0f60bf72ca2f8fc70573b75e
weak
[file "src/Directory.Build.targets"]
url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets
sha = 3a758f47e8955c15a016b3db08f383781cbdee26
etag = 22005ab28676aa6d790171003057c81fe4ceec01251626385a3dcef3417ae3cd
sha = 59861ddd1e330f3da410652b4c7fc6585726c0cf
etag = cb1f7a3e5bf85e7307407b7b6d8b9869330e2191a2317ccf6218a552e5890b08
weak
[file "src/nuget.config"]
url = https://github.com/devlooped/oss/blob/main/src/nuget.config
Expand Down Expand Up @@ -157,8 +157,8 @@
weak
[file "src/xAI.Protocol/chat.proto"]
url = https://github.com/xai-org/xai-proto/blob/main/proto/xai/api/v1/chat.proto
sha = 17a2da08cbbf89aa1f33ffe58b687fe0b2d50468
etag = e7da2c915664caf64c0da7d886826de37fcf98753cf613a6ad8aad96f6ddcda5
sha = 065692d0455244b0d5f0ccf9c05f8b98ff4618cd
etag = 8c8c9f9cc1d6991052a5058ee1a7d1829177ea9db95b9e92dc6904fc493c33c6
weak
[file "src/xAI.Protocol/deferred.proto"]
url = https://github.com/xai-org/xai-proto/blob/main/proto/xai/api/v1/deferred.proto
Expand All @@ -177,8 +177,8 @@
weak
[file "src/xAI.Protocol/image.proto"]
url = https://github.com/xai-org/xai-proto/blob/main/proto/xai/api/v1/image.proto
sha = d67bcf3e661aa9641af9750632fa1c38ea974da1
etag = 3ea27e240320c26d14b8c64d00f236c078127ebdb6fa957efc49232dfd75a20c
sha = 2c2df4f5d9429f5f21e58bb9e63285c69bcc144f
etag = 3d66f35d2e1555c6eae2218705b6f8ee5fa6537939b6ae0a18e0a2fd13a81bff
weak
[file "src/xAI.Protocol/models.proto"]
url = https://github.com/xai-org/xai-proto/blob/main/proto/xai/api/v1/models.proto
Expand All @@ -197,13 +197,13 @@
weak
[file "src/xAI.Protocol/usage.proto"]
url = https://github.com/xai-org/xai-proto/blob/main/proto/xai/api/v1/usage.proto
sha = d8def643bea79ad10f5a678d70ae37edca26490f
etag = 74c44beb7bfd2e75ea0524ff9be820cc067e009b071dee59869a16d372280a0a
sha = 2c2df4f5d9429f5f21e58bb9e63285c69bcc144f
etag = 0bf9577be87d3cfc79895971071ef1eaa78645cb784462e7e90bff03440dfff4
weak
[file "src/xAI.Protocol/video.proto"]
url = https://github.com/xai-org/xai-proto/blob/main/proto/xai/api/v1/video.proto
sha = 43a1b6b731943b8f031c2f2d946f7183f0933ffd
etag = 37562e78a6d64800b09c643632a33b6bc902955b491114bd5b6ec957d23d6e64
sha = c22ad8b1d87375ab8796b224aa56785ed922eb0d
etag = da8e45fa8f9e4ac298896e55680bb6b25a280565fd0926f18b68f17ab84cbde0
weak
[file "src/xAI.Protocol/google/protobuf/timestamp.proto"]
url = https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/timestamp.proto
Expand Down Expand Up @@ -238,6 +238,6 @@
weak
[file "src/xAI.Protocol/files.proto"]
url = https://github.com/xai-org/xai-proto/blob/main/proto/xai/api/v1/files.proto
etag = 4c91f851b288a225acfc1173f2f8853a1b550da1e97d2fdcec99debb5f8fac43
etag = b5e8ed748220b2fa727e8845f09f612cb77813d02c4260b77e6464aa0c98d57f
weak
sha = 0c0f5353aa7ab2a4ffea310f9d9364ed5c424af2
sha = c666ac39e9a8f562e94b2b57872a75bc1438ec2b
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
- `ChatOptions` mappings include `Seed`, `StopSequences`, `AllowMultipleToolCalls` → `parallel_tool_calls`, `Reasoning.Effort` → `reasoning_effort`, and `ConversationId` → `previous_response_id`. `GrokChatOptions.StoreMessages` enables stored responses and surfaces `ChatResponse.ConversationId`.
- `UsageDetails` maps `ReasoningTokenCount` and `CachedInputTokenCount` from xAI usage, plus prompt text/image/source/cost details in `AdditionalCounts`.
- Web/X search tool calls map to MEAI `WebSearchToolCallContent` / `WebSearchToolResultContent` (queries from tool arguments when present; citation URLs become `UriContent` outputs).
- The SDK pins stable `Microsoft.Extensions.AI.Abstractions` 10.10.1; its `HostedImageGenerationTool`, `ImageGenerationToolCallContent`, and `ImageGenerationToolResultContent` are used for xAI's chat image-generation tool. The optional xAI `action` is passed through `HostedImageGenerationTool.AdditionalProperties["action"]`.
- `GrokImageGenerationOptions` maps image quality and Files API storage/public-URL settings to image protocol fields. Per-image MEAI content keeps the generated protocol image as `RawRepresentation` and exposes moderation, file output, and storage errors in `AdditionalProperties`.
- xAI's experimental client-side tool-call streaming is opt-in through `GrokChatOptions.Include`. Incremental entries carry an optional `index` and may contain incomplete JSON; preserve them as raw `FunctionCallContent` fragments and never parse partial arguments as completed JSON. The upstream protocol warns against combining this option with server-side tools.
- `GrokChatOptions.SafetyIdentifier` maps to xAI's separate `safety_identifier` request field. Keep it distinct from MEAI's end-user `user` mapping; callers should hash stable user IDs and avoid personal information.
- MEAI `ReasoningEffort.ExtraHigh` maps to xAI's `EFFORT_XHIGH`. Newly synced Files and Video protocol services are exposed by `GrokClient.GetFilesClient()` / `GetVideoClient()` and `AddxAIProtocol`; no MEAI file/video abstraction is inferred from the protocol.
- TTS uses MEAI 10.10.1's native `TextToSpeechOptions.Speed`; `GrokTextToSpeechOptions` adds phrase replacement and optional character timestamps, surfaced as response/update additional properties. STT maps key terms, filler words, VAD threshold, and streaming Smart Turn options; arbitrary Opus packetization is not exposed because the current stream contract cannot preserve packet boundaries. TTS/STT WebSocket transports send the configured API key in an Authorization bearer header; keep the shared header builder covered by deterministic tests and never log credentials.
- `AsIRealtimeClient` implements MEAI 10.10.1's experimental `IRealtimeClient` over xAI's documented `/v1/realtime` WebSocket. It maps native session/audio/text/response/function-call messages and documented function, web/X search, file-search, and MCP tools; unsupported options fail explicitly and unknown server events retain raw JSON. It uses `GrokClient.HttpHandler` for ephemeral-token REST requests. The native `RealtimeSessionOptions.Voice` accepts built-in voices or custom voice IDs; `GrokRealtimeOptions.CustomVoiceId` is an explicit alias, and `EphemeralToken` overrides the API key for a session.
- xAI STT `transcript.partial` interim text is a replaceable snapshot; emit it through `AdditionalProperties["partial_text"]` rather than append-only MEAI `Contents`. Final partials and `transcript.done` are deduplicated into append-only text updates per channel, with `SessionClose` after every configured channel completes. On an xAI `error`, yield one MEAI error update and stop receiving because xAI commonly closes the socket.

## Comprehensive upstream maintenance

Expand Down
127 changes: 124 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,68 @@ var transcription = new GrokClient(Environment.GetEnvironmentVariable("XAI_API_K

var text = await transcription.GetTextAsync(File.OpenRead("audio.mp3"),
new SpeechToTextOptions { TextLanguage = "en" });

var realtime = new GrokClient(Environment.GetEnvironmentVariable("XAI_API_KEY")!)
.AsIRealtimeClient();

await using var session = await realtime.CreateSessionAsync(new GrokRealtimeOptions
{
Voice = "eve",
Instructions = "You are a helpful voice assistant.",
InputAudioFormat = new RealtimeAudioFormat("audio/pcm", 24000),
OutputAudioFormat = new RealtimeAudioFormat("audio/pcm", 24000),
VoiceActivityDetection = new VoiceActivityDetectionOptions { Enabled = true },
});

await session.SendAsync(new InputAudioBufferAppendRealtimeClientMessage(
new DataContent(File.ReadAllBytes("speech.pcm"), "audio/pcm")));
await foreach (var update in session.GetStreamingResponseAsync())
{
if (update is OutputTextAudioRealtimeServerMessage { Audio: { } audio })
Console.WriteLine(audio); // Base64 audio delta
}
```

`IRealtimeClient` provides xAI's bidirectional speech-to-speech WebSocket through
MEAI 10.10.1's experimental realtime API. The inherited `RealtimeSessionOptions.Voice`
can be a built-in voice or custom voice ID; `GrokRealtimeOptions.CustomVoiceId` is
available when the distinction is useful. For client-side connections, request a
short-lived token from a trusted server with `GrokRealtimeClient.CreateEphemeralTokenAsync`
and pass it in `GrokRealtimeOptions.EphemeralToken` rather than exposing an API key.
Custom functions, web/X search, collection file search, and hosted MCP tools in
`RealtimeSessionOptions.Tools` are mapped to xAI's documented session tools. Real-time
audio currently uses xAI's JSON/base64 transport; unsupported MEAI message/options are
rejected explicitly, and unknown server events retain their raw JSON representation.

STT streaming interim events are replaceable snapshots, so their text is available as
`AdditionalProperties["partial_text"]` and is intentionally excluded from MEAI response
contents. Final transcript updates remain append-only and can safely be aggregated
with `ToSpeechToTextResponse()` / `ToSpeechToTextResponseAsync()` without duplicating
the final `transcript.done` event. A server error is surfaced once as an error update
and ends that stream.

Use Grok-specific options for xAI's voice parameters. The stable MEAI
`TextToSpeechOptions.Speed` property controls speed, while timestamps and phrase
replacements are available through `GrokTextToSpeechOptions`:

```csharp
var audio = await speech.GetAudioAsync("Welcome to Acme Mobile.",
new GrokTextToSpeechOptions
{
Speed = 1.2f,
WithTimestamps = true,
Replace = new Dictionary<string, string> { ["Acme Mobile"] = "Acme Mobull" },
});

var characterTimings = (JsonElement)audio.AdditionalProperties!["audio_timestamps"]!;
```

`audio_duration` and `audio_timestamps` are also included in streaming audio
updates when requested. For transcription, `GrokSpeechToTextOptions` exposes
`KeyTerms`, `FillerWords`, `VadThreshold`, and (for streaming) `SmartTurn` and
`SmartTurnTimeout`. Raw Opus packet streaming is not supported by the current
stream API because it does not preserve packet boundaries.

## File Attachments

You can attach files to messages using `DataContent` to enable Grok to analyze documents,
Expand Down Expand Up @@ -166,6 +226,34 @@ Learn more about available filters at [X search parameters](https://docs.x.ai/do

You can combine both web and X search in the same request by adding both tools.

## Image Generation Tool

Grok can generate or edit images as part of a chat response using MEAI's
`HostedImageGenerationTool`:

```csharp
var response = await grok.GetResponseAsync(
"Create a poster of a red fox in a snowy forest.",
new ChatOptions { Tools = [new HostedImageGenerationTool()] });

var calls = response.Messages.SelectMany(x => x.Contents)
.OfType<ImageGenerationToolCallContent>();
```

By default, the model may generate or edit images. Use the xAI-specific `action`
additional property to restrict the tool to generation or editing:

```csharp
var imageTool = new HostedImageGenerationTool(new Dictionary<string, object>
{
["action"] = "generate", // auto | generate | edit
});
```

The adapter surfaces `ImageGenerationToolCallContent` and
`ImageGenerationToolResultContent`; inspect each result's `Outputs` or raw
protocol representation for provider-returned output details.

## Code Execution

The code execution tool enables Grok to write and execute Python code in real-time,
Expand Down Expand Up @@ -308,6 +396,22 @@ var options = new GrokChatOptions

Learn more about [Remote MCP tools](https://docs.x.ai/docs/guides/tools/remote-mcp-tools).

For experimental incremental client-side tool calls, add
`xAI.Protocol.IncludeOption.ToolCallStreaming` to `GrokChatOptions.Include`.
Intermediate updates expose each raw fragment (including its call index);
completed updates contain the full function name and arguments. xAI currently
documents this mode as unsupported with server-side tools.

For abuse attribution, set `SafetyIdentifier` to a stable hashed identifier
instead of sending an email address, name, or other personal information:

```csharp
var options = new GrokChatOptions
{
SafetyIdentifier = hashedInternalUserId,
};
```

## Image Generation

Grok also supports image generation using the `IImageGenerator` abstraction from
Expand All @@ -333,8 +437,8 @@ Console.WriteLine($"Generated image URL: {image.Uri}");

### Grok-Specific Options

Use `GrokImageGenerationOptions` to control aspect ratio and resolution — features
unique to grok-imagine models:
Use `GrokImageGenerationOptions` to control quality, aspect ratio, and resolution
or store outputs in the Files API:

```csharp
var imageGenerator = new GrokClient(Environment.GetEnvironmentVariable("XAI_API_KEY")!)
Expand All @@ -346,15 +450,27 @@ var options = new GrokImageGenerationOptions
ResponseFormat = ImageGenerationResponseFormat.Uri,
AspectRatio = ImageAspectRatio.ImgAspectRatio16_9,
Resolution = ImageResolution.ImgResolution2K,
Quality = ImageQuality.ImgQualityHigh,
Storage = new GrokImageStorageOptions
{
Filename = "city.png",
CreatePublicUrl = true,
},
};

var response = await imageGenerator.GenerateAsync(request, options);
var image = (UriContent)response.Contents.First();
Console.WriteLine($"Generated image URL: {image.Uri}");
var file = (xAI.Protocol.FileOutput)image.AdditionalProperties!["file_output"]!;
```

Aspect ratio defaults to 1:1 and resolution defaults to 1k when not specified.
2k output is generated at 1k and then upscaled with super-resolution.
Quality defaults to medium. 2k output is generated at 1k and then upscaled with
super-resolution. Per-image `AdditionalProperties` includes `file_output` when
storage succeeds, `storage_error` when upload fails, and `respect_moderation`.
`GrokImageStorageOptions.ExpiresAfterSeconds` sets file expiry; set
`PublicUrlExpiresAfterSeconds` to create an expiring public URL, or
`CreatePublicUrl = true` for a non-expiring URL.

### Editing Images

Expand Down Expand Up @@ -593,6 +709,11 @@ class MyService(Chat.ChatClient chat, Documents.DocumentsClient docs, Embedder.E
}
```

The generated `Files` and `Video` gRPC clients are available through
`GrokClient.GetFilesClient()` / `GetVideoClient()` or dependency injection
(`Files.FilesClient` and `Video.VideoClient`). These expose the upstream protocol
directly; no higher-level MEAI file or video abstraction is implied.

## Auto-updating

This project contains an automated mechanism to always fetch the latest version
Expand Down
4 changes: 3 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

<!-- Pick src-level readme+icon automatically -->
<!-- Pick src-level readme+icon automatically (icon.png preferred, logo.png fallback) -->
<PackageIcon Condition="Exists('$(MSBuildThisFileDirectory)logo.png')">logo.png</PackageIcon>
<PackageIcon Condition="Exists('$(MSBuildThisFileDirectory)icon.png')">icon.png</PackageIcon>
<PackageReadmeFile Condition="'$(PackReadme)' != 'false' and Exists('$(MSBuildThisFileDirectory)readme.md')">readme.md</PackageReadmeFile>
<!-- Pick project-level readme+icon overrides automatically -->
<PackageIcon Condition="Exists('$(MSBuildProjectDirectory)\logo.png')">logo.png</PackageIcon>
<PackageIcon Condition="Exists('$(MSBuildProjectDirectory)\icon.png')">icon.png</PackageIcon>
<PackageReadmeFile Condition="'$(PackReadme)' != 'false' and Exists('$(MSBuildProjectDirectory)\readme.md')">readme.md</PackageReadmeFile>

Expand Down
13 changes: 9 additions & 4 deletions src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,23 @@
<None Update="@(None -> WithMetadataValue('Filename', 'icon'))"
Pack="true" PackagePath="%(Filename)%(Extension)"
CopyToOutputDirectory="Never"
Condition="'$(PackageIcon)' != ''" />
Condition="'$(PackageIcon)' == 'icon.png'" />

<None Update="@(None -> WithMetadataValue('Filename', 'logo'))"
Pack="true" PackagePath="%(Filename)%(Extension)"
CopyToOutputDirectory="Never"
Condition="'$(PackageIcon)' == 'logo.png'" />

<None Update="@(None -> WithMetadataValue('Filename', 'readme'))"
Pack="true" PackagePath="%(Filename)%(Extension)"
CopyToOutputDirectory="Never"
Condition="'$(PackReadme)' != 'false' and '$(PackageReadmeFile)' != ''" />

<!-- src-level will need explicit inclusion -->
<None Include="$(MSBuildThisFileDirectory)icon.png" Link="icon.png" Visible="false"
Pack="true" PackagePath="%(Filename)%(Extension)"
<None Include="$(MSBuildThisFileDirectory)$(PackageIcon)" Link="$(PackageIcon)" Visible="false"
Pack="true" PackagePath="$(PackageIcon)"
CopyToOutputDirectory="Never"
Condition="Exists('$(MSBuildThisFileDirectory)icon.png') and !Exists('$(MSBuildProjectDirectory)\icon.png')" />
Condition="'$(PackageIcon)' != '' and Exists('$(MSBuildThisFileDirectory)$(PackageIcon)') and !Exists('$(MSBuildProjectDirectory)\$(PackageIcon)')" />

<None Include="$(MSBuildThisFileDirectory)readme.md" Link="readme.md"
Pack="true" PackagePath="%(Filename)%(Extension)"
Expand Down
26 changes: 26 additions & 0 deletions src/xAI.Protocol/ProtocolServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ public static IServiceCollection AddxAIProtocol(this IServiceCollection services

configureHttp?.Invoke(builder);

builder = services.AddGrpcClient<Files.FilesClient>(options =>
{
options.Address = address;
configureClient?.Invoke(options);
})
.AddCallCredentials((context, metadata) =>
{
metadata.Add("Authorization", $"******");
return Task.CompletedTask;
});

configureHttp?.Invoke(builder);

builder = services.AddGrpcClient<Models.ModelsClient>(options =>
{
options.Address = address;
Expand Down Expand Up @@ -120,6 +133,19 @@ public static IServiceCollection AddxAIProtocol(this IServiceCollection services

configureHttp?.Invoke(builder);

builder = services.AddGrpcClient<Video.VideoClient>(options =>
{
options.Address = address;
configureClient?.Invoke(options);
})
.AddCallCredentials((context, metadata) =>
{
metadata.Add("Authorization", $"******");
return Task.CompletedTask;
});

configureHttp?.Invoke(builder);

return services;
}
}
Loading
Loading