Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5d95472
proxy works
katzdave Oct 9, 2025
4ec2d98
proxy working, now having session description issues
katzdave Oct 9, 2025
df2ba9e
cleanup docs
katzdave Oct 9, 2025
708f6d6
Error count
katzdave Oct 14, 2025
79ba3f8
Merge branch 'main' of github.com:block/goose into error-proxy-2
katzdave Oct 15, 2025
9419ada
new interface
katzdave Oct 15, 2025
9c79b39
no testS
katzdave Oct 15, 2025
e805b3c
Merge branch 'main' of github.com:block/goose into error-proxy-2
katzdave Oct 15, 2025
cb0bc1c
improve status display
katzdave Oct 15, 2025
c27ac89
fix error
katzdave Oct 15, 2025
1fa40e0
Merge branch 'main' of github.com:block/goose into error-proxy-2
katzdave Oct 28, 2025
23518fd
Merge branch 'main' of github.com:block/goose into error-proxy-2
katzdave Oct 31, 2025
634b9b1
Fix google host
katzdave Nov 3, 2025
aef68ee
works with databricks
katzdave Nov 3, 2025
0dab0fa
Merge branch 'main' of github.com:block/goose into error-proxy-2
katzdave Nov 3, 2025
b34517c
Merge branch 'main' of github.com:block/goose into error-proxy-2
katzdave Nov 4, 2025
10788e3
Copilot import fixes
katzdave Nov 18, 2025
6e5e24d
Rm noise
katzdave Nov 18, 2025
0746f17
Merge branch 'main' of github.com:block/goose into error-proxy-2
katzdave Nov 18, 2025
7b0c7b0
test works
katzdave Nov 18, 2025
c126a72
cleanup command parser
katzdave Nov 18, 2025
6d8bb98
merge
katzdave Nov 18, 2025
a88f3c6
add uv to ci
katzdave Nov 18, 2025
4f09ea8
try explicitly waiting for proxy
katzdave Nov 18, 2025
c74ad54
preinstall deps
katzdave Nov 18, 2025
9b2d8a0
install python first:
katzdave Nov 19, 2025
0278e2b
rm dockerfile
katzdave Nov 19, 2025
fa2fe06
ad uvtoml
katzdave Nov 19, 2025
6f47acc
try uv index flag
katzdave Nov 19, 2025
53fa6f4
rm refresh
katzdave Nov 19, 2025
4a110cb
fix cleanup
katzdave Nov 19, 2025
4bbd3cf
Restore smoke tests
katzdave Nov 19, 2025
b0c9fc2
simplify
katzdave Nov 19, 2025
830246e
Merge branch 'main' of github.com:block/goose into dkatz/out-of-conte…
katzdave Nov 19, 2025
dbc0a18
better comments
katzdave Nov 21, 2025
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
8 changes: 8 additions & 0 deletions .github/workflows/pr-smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ jobs:
run: |
bash scripts/test_subrecipes.sh

- name: Set up Python (for error proxy)
uses: actions/setup-python@v5
with:
python-version: '3.12'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you still need setup-python if you are using the uv action?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah tried the uv first and it didn't seem to come with python.


- name: Install uv (for error proxy)
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # pin@v6

- name: Run Compaction Tests
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Expand Down
13 changes: 11 additions & 2 deletions crates/goose/src/providers/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,17 @@ pub trait ProviderRetry {
_ => config.delay_for_attempt(attempts),
};

tracing::info!("Backing off for {:?} before retry", delay);
sleep(delay).await;
let skip_backoff = std::env::var("GOOSE_PROVIDER_SKIP_BACKOFF")
.unwrap_or_default()
.parse::<bool>()
.unwrap_or(false);

if skip_backoff {
tracing::info!("Skipping backoff due to GOOSE_PROVIDER_SKIP_BACKOFF");
} else {
tracing::info!("Backing off for {:?} before retry", delay);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This thing is a big pain when testing with the proxy; need to be very precise and knowledgable about how many times it will backoff.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. If you want, you could turn it into a real config variable, but that's probably overkill

sleep(delay).await;
}
continue;
}

Expand Down
32 changes: 32 additions & 0 deletions scripts/provider-error-proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,38 @@ Use a custom port:
uv run proxy.py --port 9000
```

Start the proxy with an initial error mode (for automated testing):

```bash
# Start with context length error (3 times)
uv run proxy.py --mode "c 3"

# Start with rate limit error (30% of requests)
uv run proxy.py --mode "r 30%"

# Start with server error (all requests)
uv run proxy.py --mode "u *"
```

Command-line options:
- `--port PORT` - Port to listen on (default: 8888)
- `--mode COMMAND` - Initial error mode command (e.g., "c 3", "r 30%", "u *", "n")
- Same syntax as interactive commands
- `--no-stdin` - Disable stdin reader (for background/automated mode)

For automated tests or background usage, combine `--no-stdin` with `--mode`:

```bash
# Run in background for automated testing
uv run proxy.py --mode "c 3" --no-stdin &
PROXY_PID=$!

# ... run your tests ...

# Stop the proxy
kill $PROXY_PID
```

### Interactive Commands

Once the proxy is running, you can control error injection interactively:
Expand Down
186 changes: 118 additions & 68 deletions scripts/provider-error-proxy/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,73 @@ async def handle_request(self, request: Request) -> Response:
)


def parse_command(command: str) -> tuple[Optional[ErrorMode], int, float, Optional[str]]:
"""
Parse a command string and return the error mode, count, and percentage.

Args:
command: Command string (e.g., "c", "c 3", "r 30%", "u *")

Returns:
Tuple of (mode, count, percentage, error_message)
If error_message is not None, parsing failed
"""
# Parse command - remove all whitespace and parse
command_no_space = command.strip().replace(" ", "")
if not command_no_space:
return (None, 0, 0.0, "Empty command")

# Get the first character (error type letter)
error_letter = command_no_space[0].lower()

# Map letter to ErrorMode
mode_map = {
'n': ErrorMode.NO_ERROR,
'c': ErrorMode.CONTEXT_LENGTH,
'r': ErrorMode.RATE_LIMIT,
'u': ErrorMode.SERVER_ERROR
}

if error_letter not in mode_map:
return (None, 0, 0.0, f"Invalid command: '{error_letter}'. Use n, c, r, or u")

mode = mode_map[error_letter]

# Parse the rest as count or percentage
count = 1
percentage = 0.0

if len(command_no_space) > 1:
value_str = command_no_space[1:]

try:
# Check for * (100%)
if value_str == '*':
percentage = 1.0
count = 0 # Percentage mode
# Check for percentage with % sign (e.g., "30%")
elif value_str.endswith('%'):
percentage = float(value_str[:-1]) / 100.0
if percentage < 0.0 or percentage > 1.0:
return (None, 0, 0.0, f"Invalid percentage: {percentage*100:.0f}%. Must be between 0% and 100%")
count = 0 # Percentage mode
# Check if it's a decimal (percentage as 0.0-1.0)
elif '.' in value_str:
percentage = float(value_str)
if percentage < 0.0 or percentage > 1.0:
return (None, 0, 0.0, f"Invalid percentage: {percentage}. Must be between 0.0 and 1.0")
count = 0 # Percentage mode
else:
# It's an integer count
count = int(value_str)
if count < 0:
return (None, 0, 0.0, f"Invalid count: {count}. Must be >= 0")
except ValueError:
return (None, 0, 0.0, f"Invalid value: '{value_str}'. Must be an integer, decimal, percentage (30%), or * (100%)")

return (mode, count, percentage, None)


def print_status(proxy: ErrorProxy):
"""Print the current proxy status."""
mode, count, percentage = proxy.get_error_config()
Expand All @@ -562,7 +629,7 @@ def print_status(proxy: ErrorProxy):
ErrorMode.RATE_LIMIT: "⏱️ Rate limit exceeded",
ErrorMode.SERVER_ERROR: "💥 Server error (500)"
}

print("\n" + "=" * 60)
mode_str = mode_names.get(mode, 'Unknown')
if mode != ErrorMode.NO_ERROR:
Expand All @@ -589,79 +656,28 @@ def print_status(proxy: ErrorProxy):
def stdin_reader(proxy: ErrorProxy, loop):
"""Read commands from stdin in a separate thread."""
print_status(proxy)

while True:
try:
command = input("Enter command: ").strip()

if command.lower() == 'q':
print("\n🛑 Shutting down proxy...")
# Schedule the shutdown in the event loop
asyncio.run_coroutine_threadsafe(shutdown_server(loop), loop)
break

# Parse command - remove all whitespace and parse
command_no_space = command.replace(" ", "")
if not command_no_space:
continue

# Get the first character (error type letter)
error_letter = command_no_space[0].lower()

# Map letter to ErrorMode
mode_map = {
'n': ErrorMode.NO_ERROR,
'c': ErrorMode.CONTEXT_LENGTH,
'r': ErrorMode.RATE_LIMIT,
'u': ErrorMode.SERVER_ERROR
}

if error_letter not in mode_map:
print(f"❌ Invalid command: '{error_letter}'. Use n, c, r, u, or q")

# Parse the command using the shared parser
mode, count, percentage, error_msg = parse_command(command)

if error_msg:
print(f"❌ {error_msg}")
continue

mode = mode_map[error_letter]

# Parse the rest as count or percentage
count = 1
percentage = 0.0

if len(command_no_space) > 1:
value_str = command_no_space[1:]

try:
# Check for * (100%)
if value_str == '*':
percentage = 1.0
count = 0 # Percentage mode
# Check for percentage with % sign (e.g., "30%")
elif value_str.endswith('%'):
percentage = float(value_str[:-1]) / 100.0
if percentage < 0.0 or percentage > 1.0:
print(f"❌ Invalid percentage: {percentage*100:.0f}%. Must be between 0% and 100%")
continue
count = 0 # Percentage mode
# Check if it's a decimal (percentage as 0.0-1.0)
elif '.' in value_str:
percentage = float(value_str)
if percentage < 0.0 or percentage > 1.0:
print(f"❌ Invalid percentage: {percentage}. Must be between 0.0 and 1.0")
continue
count = 0 # Percentage mode
else:
# It's an integer count
count = int(value_str)
if count < 0:
print(f"❌ Invalid count: {count}. Must be >= 0")
continue
except ValueError:
print(f"❌ Invalid value: '{value_str}'. Must be an integer, decimal, percentage (30%), or * (100%)")
continue


# Set the error mode
proxy.set_error_mode(mode, count, percentage)
print_status(proxy)

except EOFError:
# Handle Ctrl+D
print("\n🛑 Shutting down proxy...")
Expand Down Expand Up @@ -716,7 +732,17 @@ def main():
default=8888,
help='Port to listen on (default: 8888)'
)

parser.add_argument(
'--mode',
type=str,
help='Error mode command (e.g., "c 3", "r 30%%", "u *", "n")'
)
parser.add_argument(
'--no-stdin',
action='store_true',
help='Disable stdin reader (for background/automated mode)'
)

args = parser.parse_args()

print("=" * 60)
Expand All @@ -735,14 +761,38 @@ def main():

# Create proxy instance
proxy = ErrorProxy()


# Set initial error mode from command-line arguments
if args.mode:
mode, count, percentage, error_msg = parse_command(args.mode)

if error_msg:
print(f"❌ Error parsing --mode argument: {error_msg}")
print(f" Example usage: --mode \"c 3\" or --mode \"r 30%\"")
return

proxy.set_error_mode(mode, count, percentage)
print()
print(f"Initial mode set from command-line arguments:")
print(f" Mode: {mode.name}")
if percentage > 0.0:
print(f" Percentage: {percentage*100:.0f}%")
elif count > 0:
print(f" Count: {count}")
print()

# Create event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

# Start stdin reader thread
stdin_thread = threading.Thread(target=stdin_reader, args=(proxy, loop), daemon=True)
stdin_thread.start()

# Start stdin reader thread only if not disabled
if not args.no_stdin:
stdin_thread = threading.Thread(target=stdin_reader, args=(proxy, loop), daemon=True)
stdin_thread.start()
else:
print("Running in no-stdin mode (background/automated)")
print("Use SIGINT (Ctrl+C) or SIGTERM to stop the proxy")
print()

# Create and run the app
app = loop.run_until_complete(create_app(proxy))
Expand Down
Loading
Loading