Repository navigation
Grok gRPC fallback, xAI dedupe, iOS canonical cards, dedupe #306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jaywedgeworth22
wants to merge
6
commits into
main
Choose a base branch
from
opencode/grok-xai-ios
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+724
−130
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
db0cde1
Grok gRPC fallback, settings tier, xAI dedupe, iOS canonical cards, M…
8699fe0
Merge origin/main into opencode/grok-xai-ios (PR #306)
5e5832a
Address kody review on PR #306 and repair the four red tests
jaywedgeworth22 69b0656
Fix inout binding for the NaN percentage fixture
jaywedgeworth22 e7adada
Match the Grok credits pool on exact id components
jaywedgeworth22 0b23d18
Merge origin/main (#307) into opencode/grok-xai-ios
jaywedgeworth22 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,256 @@ | ||
| import Foundation | ||
|
|
||
| /// Best-effort parser for the grok.com billing gRPC-web fallback | ||
| /// (`grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig`), per the wire | ||
| /// contract documented by CodexBar (steipete/CodexBar, docs/grok.md, | ||
| /// descriptor checked 2026-09-21): | ||
| /// | ||
| /// - Request body is one gRPC-web frame carrying | ||
| /// `GetGrokCreditsConfigRequest { exclude_legacy_monthly_usage: false }`: | ||
| /// bytes `00 00 00 00 02 08 00`. An empty frame is rejected with gRPC | ||
| /// status 13, so the explicit false matters. | ||
| /// - The response config is field 1 of the reply (or the reply itself in | ||
| /// the raw form some successful requests return). Inside it, a top-level | ||
| /// fixed32 float is the credit usage percent, repeated field 7 entries | ||
| /// are per-product shares `{1: product id, 2: float percent}`, and the | ||
| /// current period is a submessage of two protobuf Timestamps. | ||
| /// - grok.com now expects a browser-held Web Key Exchange keypair, so a | ||
| /// bearer-only probe can fail with gRPC status 16 (`no-credentials`). | ||
| /// This parser is therefore strictly a fallback: it only ever fills a | ||
| /// percentage the CLI-proxy answer did not publish, and a malformed or | ||
| /// partial reply yields nil rather than a guessed number. | ||
| public enum GrokGrpcCredits { | ||
| public struct Reading: Equatable, Sendable { | ||
| /// Percent of the subscription credit pool already used. | ||
| public var usedPercent: Double | ||
| /// Current period bounds, when a complete active period was present. | ||
| public var periodStart: Date? | ||
| public var periodEnd: Date? | ||
| } | ||
|
|
||
| /// The exact request frame bytes for the fallback call. | ||
| public static let requestFrame = Data([0x00, 0x00, 0x00, 0x00, 0x02, 0x08, 0x00]) | ||
|
|
||
| /// Field 6 of the config is the current period, a `{start, end}` pair of | ||
| /// Timestamps. Period decoding is pinned to this number rather than to | ||
| /// "any length-delimited field that happens to hold two Timestamps", so | ||
| /// an unrelated submessage can never invent bounds for the window. | ||
| static let currentPeriodField = 6 | ||
|
|
||
| /// Parse a gRPC-web (framed) or raw protobuf reply. `now` bounds the | ||
| /// implicit-zero rule: a complete active period with no percentage field | ||
| /// anywhere on the wire reads as zero usage, matching grok.com's own web | ||
| /// client, whose descriptor declares the percent an implicit-presence | ||
| /// float. | ||
| public static func parse(_ data: Data, now: Date) -> Reading? { | ||
| guard let message = unwrapFrames(data) else { return nil } | ||
| // Reply form: field 1 is the config. Raw form: the reply IS the | ||
| // config. Prefer the wrapped form when field 1 decodes as bytes. | ||
| let config: Data | ||
| if let fields = try? ProtobufScanner.fields(message), | ||
| let wrapped = fields.first(where: { $0.number == 1 && $0.wire == .bytes }) { | ||
| config = wrapped.bytes | ||
| } else { | ||
| config = message | ||
| } | ||
| guard let fields = try? ProtobufScanner.fields(config) else { return nil } | ||
|
|
||
| var percent: Double? | ||
| var sawPercentField = false | ||
| var periodStart: Date? | ||
| var periodEnd: Date? | ||
| for field in fields { | ||
| switch field.wire { | ||
| case .fixed32: | ||
| // The only top-level floats in the config are usage | ||
| // percentages; product shares nest inside field 7 entries. | ||
| guard let value = field.floatValue else { continue } | ||
| // Presence is tracked apart from the accepted value. A | ||
| // published percentage that is out of range or non-finite is | ||
| // still a published percentage, and must not reach the | ||
| // implicit-zero rule below as if nothing had been published. | ||
| sawPercentField = true | ||
| if percent == nil, value.isFinite, (0...100).contains(value) { | ||
| percent = value | ||
| } | ||
|
jaywedgeworth22 marked this conversation as resolved.
|
||
| case .bytes: | ||
| // First match wins: a later field-6 submessage is not allowed | ||
| // to overwrite bounds an earlier one already supplied. | ||
| guard field.number == Self.currentPeriodField, periodStart == nil, | ||
| let period = timestampPair(field.bytes), | ||
| Self.isRecognizedPeriod(period.start, period.end) else { continue } | ||
| periodStart = period.start | ||
| periodEnd = period.end | ||
| case .varint, .fixed64: | ||
| continue | ||
| } | ||
| } | ||
|
|
||
| if let percent { | ||
| return Reading(usedPercent: percent, periodStart: periodStart, periodEnd: periodEnd) | ||
| } | ||
| // Implicit zero: complete active period, and no percentage field on | ||
| // the wire at all. A rejected percentage is not an absent one. | ||
| if !sawPercentField, let start = periodStart, let end = periodEnd, | ||
| start <= now, now <= end { | ||
| return Reading(usedPercent: 0, periodStart: start, periodEnd: end) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| /// The credits pool is billed weekly or monthly. A Timestamp pair of any | ||
| /// other length is not this field's subscription period, whatever its | ||
| /// shape, and is left uncaptured rather than used as a reset bound. | ||
| private static func isRecognizedPeriod(_ start: Date, _ end: Date) -> Bool { | ||
| let days = end.timeIntervalSince(start) / 86_400 | ||
| return (6.5...8).contains(days) || (27...32).contains(days) | ||
| } | ||
|
|
||
| /// Whether the reply's gRPC-web trailer frame reports status 16 | ||
| /// (`UNAUTHENTICATED`). grok.com answers a probe it will not serve with | ||
| /// a framed 200 and a `grpc-status: 16` trailer, so an HTTP status check | ||
| /// alone reads a refused credential as an empty payload. Only the trailer | ||
| /// carries the status, which is why this lives beside the frame parser. | ||
| public static func isUnauthenticated(_ data: Data) -> Bool { | ||
| let bytes = [UInt8](data) | ||
| var offset = 0 | ||
| var trailer = [UInt8]() | ||
| while offset + 5 <= bytes.count { | ||
| let flag = bytes[offset] | ||
| guard flag == 0x00 || flag == 0x80 else { break } | ||
| let length = Int(bytes[offset + 1]) << 24 | Int(bytes[offset + 2]) << 16 | ||
| | Int(bytes[offset + 3]) << 8 | Int(bytes[offset + 4]) | ||
| guard offset + 5 + length <= bytes.count else { break } | ||
| if flag == 0x80 { trailer.append(contentsOf: bytes[(offset + 5)..<(offset + 5 + length)]) } | ||
| offset += 5 + length | ||
| } | ||
| guard !trailer.isEmpty else { return false } | ||
| let text = String(decoding: trailer, as: UTF8.self).lowercased() | ||
| guard let marker = text.range(of: "grpc-status:") else { return false } | ||
| return text[marker.upperBound...].prefix(while: \.isNumber) == "16" | ||
| } | ||
|
|
||
| /// A `{start: Timestamp, end: Timestamp}` submessage, when the bytes | ||
| /// decode as exactly two Timestamp-shaped fields in order. | ||
| private static func timestampPair(_ data: Data) -> (start: Date, end: Date)? { | ||
| guard let fields = try? ProtobufScanner.fields(data), fields.count == 2, | ||
| fields.allSatisfy({ $0.wire == .bytes }) else { return nil } | ||
| guard let start = timestamp(fields[0].bytes), let end = timestamp(fields[1].bytes), | ||
| start < end else { return nil } | ||
| return (start, end) | ||
| } | ||
|
|
||
| /// A protobuf Timestamp: field 1 varint seconds, optional field 2 nanos. | ||
| private static func timestamp(_ data: Data) -> Date? { | ||
| guard let fields = try? ProtobufScanner.fields(data), | ||
| let secondsField = fields.first(where: { $0.number == 1 && $0.wire == .varint }), | ||
| let seconds = secondsField.varintValue, | ||
| (1_500_000_000...2_000_000_000).contains(seconds) else { return nil } | ||
| return Date(timeIntervalSince1970: TimeInterval(seconds)) | ||
| } | ||
|
|
||
| /// Strip gRPC-web framing: `[flag][4-byte big-endian length][payload]` | ||
| /// repeated, where flag 0x80 marks a trailer frame. Data that is not | ||
| /// framed is returned as-is (the raw protobuf form). | ||
| private static func unwrapFrames(_ data: Data) -> Data? { | ||
| guard data.count >= 5 else { return data.isEmpty ? nil : data } | ||
| var offset = 0 | ||
| var payload = Data() | ||
| var framed = false | ||
| let bytes = [UInt8](data) | ||
| while offset + 5 <= bytes.count { | ||
| let flag = bytes[offset] | ||
| guard flag == 0x00 || flag == 0x80 else { break } | ||
| let length = Int(bytes[offset + 1]) << 24 | Int(bytes[offset + 2]) << 16 | ||
| | Int(bytes[offset + 3]) << 8 | Int(bytes[offset + 4]) | ||
| guard offset + 5 + length <= bytes.count else { break } | ||
| framed = true | ||
| if flag == 0x00 { payload.append(contentsOf: bytes[(offset + 5)..<(offset + 5 + length)]) } | ||
| offset += 5 + length | ||
| } | ||
| return framed && offset == bytes.count ? payload : data | ||
| } | ||
| } | ||
|
|
||
| /// Minimal protobuf wire scanner: enough to walk a message's top level | ||
| /// without a generated model. Malformed input throws; callers treat that | ||
| /// as "no reading", never as a partial one. Varints stop after ten bytes | ||
| /// and reject overflow, and length-delimited reads cannot exceed the | ||
| /// enclosing message — the same bounds CodexBar's reader documents. | ||
| enum ProtobufScanner { | ||
| enum Wire { | ||
| case varint, fixed64, bytes, fixed32 | ||
| } | ||
|
|
||
| struct Field { | ||
| var number: Int | ||
| var wire: Wire | ||
| var varintValue: UInt64? | ||
| var floatValue: Double? | ||
| var bytes: Data | ||
| } | ||
|
|
||
| enum ScanError: Error { case malformed } | ||
|
|
||
| static func fields(_ data: Data) throws -> [Field] { | ||
| var reader = Reader(bytes: [UInt8](data)) | ||
| var fields: [Field] = [] | ||
| while !reader.atEnd { | ||
| let tag = try reader.varint() | ||
| let number = Int(tag >> 3) | ||
| guard number > 0 else { throw ScanError.malformed } | ||
| switch tag & 0x07 { | ||
| case 0: | ||
| let value = try reader.varint() | ||
| fields.append(Field(number: number, wire: .varint, varintValue: value, bytes: Data())) | ||
| case 1: | ||
| let raw = try reader.take(8) | ||
| fields.append(Field(number: number, wire: .fixed64, bytes: Data(raw))) | ||
| case 2: | ||
| let length = try reader.varint() | ||
| guard length <= UInt64(reader.remaining) else { throw ScanError.malformed } | ||
| let raw = try reader.take(Int(length)) | ||
| fields.append(Field(number: number, wire: .bytes, bytes: Data(raw))) | ||
| case 5: | ||
| let raw = try reader.take(4) | ||
| let bits = UInt32(raw[0]) | UInt32(raw[1]) << 8 | UInt32(raw[2]) << 16 | UInt32(raw[3]) << 24 | ||
| fields.append(Field(number: number, wire: .fixed32, | ||
| floatValue: Double(Float(bitPattern: bits)), bytes: Data(raw))) | ||
| default: | ||
| throw ScanError.malformed | ||
| } | ||
| } | ||
| return fields | ||
| } | ||
|
|
||
| private struct Reader { | ||
| var bytes: [UInt8] | ||
| var offset = 0 | ||
| var atEnd: Bool { offset >= bytes.count } | ||
| var remaining: Int { bytes.count - offset } | ||
|
|
||
| mutating func varint() throws -> UInt64 { | ||
| var result: UInt64 = 0 | ||
| var shift: UInt64 = 0 | ||
| for _ in 0..<10 { | ||
| guard offset < bytes.count else { throw ScanError.malformed } | ||
| let byte = bytes[offset] | ||
| offset += 1 | ||
| if shift == 63 { | ||
| // Tenth byte may only contribute bit 63. | ||
| guard byte <= 1 else { throw ScanError.malformed } | ||
| } | ||
| result |= UInt64(byte & 0x7F) << shift | ||
| if byte & 0x80 == 0 { return result } | ||
| shift += 7 | ||
| } | ||
| throw ScanError.malformed | ||
| } | ||
|
|
||
| mutating func take(_ count: Int) throws -> [UInt8] { | ||
| guard count >= 0, offset + count <= bytes.count else { throw ScanError.malformed } | ||
| defer { offset += count } | ||
| return Array(bytes[offset..<(offset + count)]) | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.